diff --git a/AGENTS.md b/AGENTS.md index ebf80b2b35..1940dfde10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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- -b main && cd ../objectstack- && 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: @@ -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: @@ -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 @@ -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*: