Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,11 +173,19 @@ Other scripts: `objectui:bump` (pull only), `objectui:build`, `objectui:clean`.
11. **Worktree-first — never edit on the shared `main` checkout.** This repo is edited by **multiple agents at once**; the shared tree has its HEAD switched and reset *under you*, silently clobbering uncommitted work — a feature branch on the *shared* checkout is **not** enough (it still gets switched under you). Before your **first file edit**, be in a dedicated worktree on a feature branch: `git worktree add ../objectstack-<task> -b <branch> main && cd ../objectstack-<task> && pnpm install`. Two PreToolUse hooks **enforce** this — `.claude/hooks/guard-main-checkout.sh` blocks `Edit`/`Write`/`NotebookEdit`, and `.claude/hooks/guard-main-checkout-bash.sh` blocks the identical write arriving through **Bash** (`>`/`>>` redirection, `sed -i`, `perl -i`, `tee`, `cp`, `mv`, `rm`, `touch`) — and both check the **target file's own repo**, so sibling repos (`objectui`/`cloud`) you touch are covered too (deliberate non-task override: `OS_ALLOW_MAIN_EDITS=1`, one switch for both). The Bash guard is precision-first: it never blocks reads, and any shape it cannot resolve with confidence (`bash -c …`, `xargs`, `node -e`, a `$VAR`/glob target) is allowed through — the rule still outranks the hook. **The one thing a worktree does *not* isolate is the stash** — `refs/stash` lives in the **common** `.git`, shared by every worktree; a third hook (`guard-shared-stash.sh`, `OS_ALLOW_STASH=1`) blocks the mutating forms, and the collision-free replacements are in Multi-agent discipline below.
12. **Contract-first — fix the metadata, not the runtime.** This is a metadata-driven framework: `packages/spec` is the one contract between metadata *producers* and the runtime/renderers that *consume* it. When a piece of metadata "doesn't work," ask **first**: *is it spec-compliant? is this the long-term-correct direction?* If the metadata is wrong, fix it at the **producer** and **reject it at authoring/publish** (validation / lint) so the error surfaces loudly — do **not** add a lenient alias or `??` fallback in a consumer (a node executor, the REST layer, a renderer) to tolerate off-spec input. A tolerant fallback fossilizes the wrong convention into a second de-facto contract, dilutes the spec, and hides the producer's bug — one strict contract beats N dialects. This is an **internal** contract (we own both ends), so "be liberal in what you accept" (Postel) does **not** apply — that's for untrusted boundaries. Change the **spec** only when the spec itself is genuinely wrong, and then deliberately (edit the Zod schema + migrate), never by accreting consumer-side fallbacks. When an alias must be tolerated at all, declare it as an **ADR-0087 conversion-layer entry** (never a bare `??`, and no executor shims) so it is declared, loud, tested, and *removable on a schedule* — the `cfg.filter ?? cfg.filters`-style fallbacks the flow executors once carried were all paid down exactly that way, emptying and deleting the executor shim that read them. Stored `sys_metadata` rows (data at rest) are covered from the other side: every rehydration seam replays the **full** conversion chain — retired entries included — via `applyConversionsToStoredItem` (ADR-0087 addendum), so a consumer never needs its own accommodation for a legacy stored shape either. *Worked example:* an AI-authored flow node used wrong key names and template syntax for what the executor reads → the fix was correcting the authoring skill + a publish-gate lint that rejects the wrong shape, **not** a runtime alias in the executor (that alias was proposed and rejected). Strengthens #5.
13. **An accepted ADR binds until a superseding ADR says otherwise.** Reversing a recorded decision is itself a decision: it needs a **new ADR** (or an amended status line on the old one), not a changeset that quietly does the opposite. Before changing behaviour in `docs/adr/`-governed territory, **grep the ADRs for the surface you are touching** — the decision is often older and broader than the code comment in front of you. A reversal of three accepted ADRs once landed as a patch-level changeset and held for a day; the mechanism was not carelessness — **the file being edited never named the ADRs that governed it**, so the author could not have known. Hence the corollary: when you implement an ADR's decision, **leave its id in the code**, and anchor load-bearing spots in `scripts/adr-anchors/` (`pnpm check:adr-anchors`) — **one new JSON file per anchor, named for the path it anchors; there is no index to register it in** — so the next author is told which decision they are standing on. A decision nobody can find is a decision that will be reversed.
14. **⛔ An ADR is confirmed and merged by the maintainer, by hand — no AI seat merges, queues, or arms auto-merge on a `docs/adr/**` PR.** Maintainer ruling, 2026-08-08, verbatim and untranslated:
14. **⛔ A governed surface is confirmed and merged by the maintainer, by hand — no AI seat merges, queues, or arms auto-merge on a PR whose diff touches one.** Three maintainer rulings, verbatim and untranslated — each widens the one before it, and the third *is* the current definition of the surface:

> **adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。**
> **adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。** (2026-08-08)

**Authoring stays open to every seat** — drafting an ADR, pushing the branch, opening the PR, revising it under review. What is reserved is the **landing**: on any PR whose diff touches `docs/adr/**`, ⛔ never merge it, ⛔ never add it to the merge queue, ⛔ never call `enable_pr_auto_merge`. Judge it on the PR's **file list**, not on its description, and a **mixed diff is not a proportion question** — one path hit is enough; if the rest needs to land, split the ADR into its own PR. **Reviewed + approved + fully green does not override this.** Under #13 an accepted ADR *is* the decision, so merging one is the act of adopting a governance position — the one class of change about which "CI is green" carries no information at all (a thorough, fully-green ADR draft has been closed by the maintainer on demand grounds no gate could evaluate). **Already armed or queued when you read this?** ⚠️ Converting the PR back to **draft** is the only action that reliably removes it from the merge queue; `disable_pr_auto_merge` alone drops the arming but **not** queue membership. Do both, then confirm from the remote that it is in neither the queue nor `origin/main` (§7's draft-flip re-arm note, run backwards). ⚠️ **And do not read draft as a barrier that holds by itself** — a drafted ADR PR has nevertheless been merged, twice, by two different AI seats within one hour of the ruling above (both ratified retroactively, explicitly setting no precedent). The barrier is **this directive**, and it is the only pre-merge barrier there is — the per-PR approval check that used to sit beside it retired under the maintainer's 2026-08-18 ruling that a human merge IS the review record for a governed surface (it was red on every governed PR by design, sat outside the required-context set, and never blocked anything). Behind the directive sits detection, not prevention: `docs/adr/` in CODEOWNERS routes review requests, and the report-only post-merge audit (`scripts/pm/check-governed-merges.mjs`, whose header carries this rule's incident history) lists every governed-surface merge for the PM round report — an entry the maintainer does not recognise is a seat violation, filed and rolled back. A seat that has read this far is not thereby licensed to judge an exception; the rule has no exception to judge.
> **所有 skills 的更新和 adr 类似,需要人工审核** (2026-08-11)

> **`docs/adr/**` + `.claude/**`(含 agents/hooks/settings,不只 skills)+ `skills/**` + `AGENTS.md` + `CLAUDE.md`。混合 diff 照现行规则一条命中即整 PR 分叉** (2026-08-18)

**Which surfaces — and where that list actually lives.** The 2026-08-18 quotation above *is* the definition, and it is wider than ADRs and wider than skills: the two repo-root instruction files are on it, so **the file you are reading is itself a governed surface**, and so is `.claude/` entire — agents, hooks and settings, not only `.claude/skills/`. ⚠️ Even so, treat that quotation as a reading aid rather than the register: the set has grown three times in two days. The register is the `GOVERNED_SURFACES` table in `scripts/pm/check-governed-merges.mjs`, and adding a surface is an edit *there*, never here. Print today's set rather than trusting this paragraph: `node -e "import('./scripts/pm/check-governed-merges.mjs').then(m=>console.log(m.GOVERNED_SURFACES.map(s=>s.glob).join(' · ')))"`

**Authoring stays open to every seat** — drafting the ADR, the skill or the instruction edit, pushing the branch, opening the PR, revising it under review. What is reserved is the **landing**: on any PR whose diff touches a governed surface, ⛔ never merge it, ⛔ never add it to the merge queue, ⛔ never call `enable_pr_auto_merge`, ⛔ never flip it out of draft to make any of those possible. Judge it on the PR's **file list**, not on its description, and a **mixed diff is not a proportion question** — one path hit is enough; if the rest needs to land, split the governed files into their own PR. **Reviewed + approved + fully green does not override this.** Under #13 an accepted ADR *is* the decision, so merging one is the act of adopting a governance position — the one class of change about which "CI is green" carries no information at all (a thorough, fully-green ADR draft has been closed by the maintainer on demand grounds no gate could evaluate). The other surfaces are reserved for a reason of the same shape: the agent instruction tree and these two root files are the operating protocol every *later* dispatch reads, and the published catalog lands in codebases this repo cannot see, so a bad merge propagates into work nobody has started yet — and green says nothing about whether it should propagate.

**Already armed or queued when you read this?** ⚠️ Converting the PR back to **draft** is the only action that reliably removes it from the merge queue; `disable_pr_auto_merge` alone drops the arming but **not** queue membership. Do both, then confirm from the remote that it is in neither the queue nor `origin/main` (§7's draft-flip re-arm note, run backwards). ⚠️ **And do not read draft as a barrier that holds by itself** — a drafted ADR PR has nevertheless been merged, twice, by two different AI seats within one hour of the first ruling above (both ratified retroactively, explicitly setting no precedent), and a skill PR whose own body said it was awaiting a human merge was flipped ready by an unidentified seat and landed by the merge queue with zero reviews of any kind. **The barrier is this directive, and it is the only pre-merge barrier there is.** The per-PR approval check that used to sit beside it retired under the maintainer's 2026-08-18 ruling that a human merge IS the review record for a governed surface — 「人工合并即人工审核」 — because it was red on every governed PR by design, sat outside the required-context set, and so never blocked anything. ⛔ Do not read that retirement as a relaxation: it removed a check that was not holding, and left the discipline carrying the whole load. Behind the directive sits **detection, not prevention**: `docs/adr/` in CODEOWNERS routes review requests — and it is the *only* governed surface routed there, so on the other four nothing summons the maintainer automatically — while the report-only post-merge audit (`scripts/pm/check-governed-merges.mjs`, whose header carries this rule's incident history) lists every governed-surface merge for the PM round report. Every entry on that list should be a merge the maintainer performed or ordered in person; one he does not recognise is a seat violation, filed and rolled back. A seat that has read this far is not thereby licensed to judge an exception; the rule has no exception to judge.

15. **⛔ A version release is performed by the maintainer, by hand — no AI seat publishes, tags, cuts a Release, or triggers a release workflow, and none merges the Version Packages PR.** Maintainer ruling, 2026-08-07, verbatim and untranslated:

Expand DownExpand Up@@ -325,9 +333,12 @@ Even inside your own worktree, operate defensively:
**arm only what is already green and accepted.**

⛔ **Two classes of PR never enter this path, however green:** (a) a diff touching
`docs/adr/**` (**Prime Directive #14**); (b) the **Version Packages** PR, or any PR
whose merge performs a release (**Prime Directive #15**). Read the PR's file list
(`get_files`) **and its author** before you arm anything.
any **governed surface** (**Prime Directive #14**, which names them and holds the
current list) — that list is much longer than `docs/adr/**`, it grew three times in
two days, and **this file and `CLAUDE.md` are on it**, so re-read it rather than
recalling it; (b) the **Version Packages** PR, or any PR whose merge performs a
release (**Prime Directive #15**). Read the PR's file list (`get_files`) **and its
author** before you arm anything.

**Green means the gate-carrying jobs' `conclusion` is `success`** — not "no failure
yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it:
Expand DownExpand Up@@ -642,6 +653,11 @@ agent playbooks live in `.claude/skills/` and must carry `metadata.internal: tru
`dogfood-verification` (boot and drive the real app in a browser) and
`spec-property-retirement` (ADR-0049 enforce-or-remove — the full retirement kit).

⛔ **Both roots are governed surfaces**: a PR touching either is human-merge only and may
never be queued, armed or flipped out of draft — **Prime Directive #14**. No per-PR check
holds this any more; the discipline is the whole barrier, and a post-merge audit lists
what landed.

---

## Patterns
Expand DownExpand Up@@ -908,10 +924,10 @@ new open registry? Add it to `OPEN_CAPABILITY_REGISTRIES` in the same PR that fi
a feature branch, commit, push, open a PR, and — once remote CI is fully green and
the PR is accepted — arm auto-merge so the queue lands it (Multi-agent discipline
§7: never straight to `main`; never arm a PR that isn't green yet). A finished task
= a merged PR, not a dirty working tree. ⛔ **Except a diff touching `docs/adr/**`**:
push it, open the PR, and stop there — landing it is the maintainer's, by hand
(Prime Directive #14). For that one class, a finished task = a PR left visibly
awaiting a human merge.
= a merged PR, not a dirty working tree. ⛔ **Except a diff touching a governed
surface** (Prime Directive #14 names them — more than ADRs): push it, open the PR,
and stop there, landing it is the maintainer's, by hand. For that class, a
finished task = a PR left visibly awaiting a human merge.
3. **Add a changeset for feature work.** When the change is a feature or functional improvement, run `pnpm changeset` (or add a `.changeset/*.md` entry) describing it before committing. Pure bug fixes do **not** require a changeset.
**Breaking changesets must carry their migration.** If the change removes or renames anything an author can write (a spec key, an export, a config field), the changeset body must state the FROM → TO mapping and the one-line fix — this text ships to consumers as `CHANGELOG.md` inside the npm package and is what an upgrading agent greps after the tombstone error. Removing an authorable spec key also requires a tombstone so the rejection itself carries the prescription — `retiredKey()` (`packages/spec/src/shared/retired-key.ts`) on a non-strict schema, or an entry in the relevant `UNKNOWN_KEY_GUIDANCE` / `*_RETIRED_KEY_GUIDANCE` map (see `object.zod.ts`, `ai/tool.zod.ts`) when the schema is `.strict()`. The changeset is one of fourteen surfaces a retirement touches — follow the `spec-property-retirement` skill (`.claude/skills/`) rather than reconstructing the kit, and note the two routes imply **opposite** liveness-ledger dispositions.
**A breaking changeset must also state its ADR-0087 disposition, in writing.** Add exactly one marker to the changeset body — `pnpm check:adr-0087-registration` enforces it, and the CI step is *Require an ADR-0087 disposition on a declared-breaking changeset*:
Expand Down
Loading