Skip to content

feat(objectql)!: a tool holds one name per package (bare-name one holder, after the AI family) - #31661

Merged
os-zhuang merged 5 commits into
mainfrom
claude/project-thread-7w4hop
Nov 9, 2026
Merged

os-zhuang merged 5 commits into
mainfrom
claude/project-thread-7w4hop

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #31414

tool joins the bare-name one-holder list after the AI family of #30803 (PR #31375), by the maintainer's acceptance of a separate card for it. Claim 6725057016 (domain:server#1, card-thread session_01ADHkmS1DVx9CH1xvXikQBW).

Clause-②: no (narrowing) — measured on head 30b6c84f and, for base 69244406, by the ablation that puts base's list back: the engine's package door (SchemaRegistry.installPackage) and item seam (SchemaRegistry.registerItem) refuse, after the cold boot, a package registration they accepted on base: one whose tool bears a name another installed package holds (base list: install-local answers 200 and the tool is shadowed; head: 422 PLUGIN_REGISTER_FAILED). Nothing is widened.
Metadata-protocol: narrowed
Skills-docs: none — skills/** does not teach that two packages may share a tool name (grep: zero hits), and the naming rule itself is phase 2's (#30805); the prescription ships in the refusal and warning text and in ADR-0048's dated note P1.10.

⚠️ Tier H: this diff touches docs/adr/0048-cross-package-metadata-collision.md (a new dated note, P1.10, under the 2026-11-06 addendum, and tool named in the header's Addenda entry, §3.4's 2026-11-06 note, P1.5 and P1.9; ⛔ not the Status line, which draft PR #31586 edits). The seat routes the approver.

维护者速读

  • 改了什么:tool 加入"一个名字只认一个包"的列表(排在 agent、skill 之后)。冷启动结束后,第二个包再声明同名 tool,安装门和注册口都拒绝,返回 422 NAMESPACE_CONFLICT,点出两个持有包和改名建议;已经同名的部署照常启动、只告警一次。
  • 为什么改:tool 的所有读者都只按裸名找它:/meta/tool/NAME 返回先注册包的那个;skill 的 tools 列表只能写裸名;MCP 服务器一个名字只能注册一个 tool;tool 调用协议里没有包。所以两个同名 tool 必然有一个被静默遮蔽。
  • 风险与回滚:只收窄接受集,不改已存数据;示例应用和平台包都没声明 tool,现有部署不会启动失败(冷启动只告警)。回滚即 revert 本 PR。
  • 先看:packages/objectql/src/bare-name-one-holder.ts(列表末尾加 tool 及其理由短句)、ADR-0048 P1.10、registry-bare-name-one-holder-ai-family.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/registry-bare-name-one-holder-ai-family.test.ts —— 预期 38 passed。
  • 需要你做的:ADR 改动是 Tier H,需要审批人批准后由席位落地。

What changes

  • packages/objectql/src/bare-name-one-holder.ts — tool joins BARE_NAME_ONE_HOLDER_TYPES last, after the AI family (BARE_NAME_REGISTRY_TYPES stays derived): its collection key (tools), consumer clause ("a skill's tools and an MCP client name a tool") and noun, empty built-in and overridable-baseline sets. Module doc: the measurement.
  • packages/objectql/src/registry.ts, packages/objectql/src/plugin.ts — comments name tool among the covered types; no code change. Both doors and the cold-boot seal are generic over the list.
  • packages/objectql/src/registry-bare-name-one-holder-ai-family.test.ts — tool runs through the AI family's matrix (it is the shared harness for these types). Forced edits, named: the whole-list cell now ends in 'tool'; the declaredBareNames cell's "a tool declares nothing" now asserts tools declare (nested plugin included); the CONTROL "two packages' tools under one name both install after the seal" is replaced, since that is now the refused case, by CONTROLs for different tool names across packages, a tool versus another package's skill or agent under one name, and one package's skill and tool under one name.
  • packages/qa/dogfood/test/bare-name-one-holder-ai-family.dogfood.test.ts — forced edits, named: the cold-boot cell asserted the warning never names the tool; it now asserts it does (with its rename) and that /meta/tool/NAME keeps serving alpha; the CONTROL epsilon installed the shared tool name and now declares a fresh one. New: install-local of zeta declaring the held tool name is 422 PLUGIN_REGISTER_FAILED.
  • docs/adr/0048-cross-package-metadata-collision.md (Tier H) — new dated note P1.10 (2026-11-08) under the 2026-11-06 addendum: the measurement (this repo's readers and the cloud reading), what is refused, the cold boot. tool is also named in the header's Addenda entry, §3.4's 2026-11-06 note (and dropped from its "keeps §3.4" list), P1.5 and P1.9's "Not in the list" (seat ruling B on the claim, restated 6726705019). The Status line is untouched.
  • scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json — the invariant names tool after the AI family.
  • .changeset/31414-bare-name-tool.md — @objectstack/objectql minor, both declaration lines (the first restated no (narrowing) per the seat's ruling), BREAKING Why / Before / Now / The cold boot / Who moves / Unchanged, adr-0087: not-required (no-migration-prescription), measured producers.

Why (measured)

Step 1 of the card, how a tool is resolved by name. No cloud read access in this container (attaching objectstack-ai/cloud was refused), so the cloud question went to repo:cloud#3 on mailbox #31366 (seat's comment 6725074004), answered on the card (6725094995, read at cloud ba6f0fa). Both sides:

Reader Where Package context
GET /meta/tool/NAME protocol by-name read none: first-registered package's tool, silently, either order; a third package after the boot installs 200, shadowed (PR #31375 table, re-pinned here by the dogfood file)
a skill's tools list packages/spec/src/ai/skill.zod.ts SkillSchema.tools none: the regex admits a bare snake_case name only; "bound by the cloud agent runtime"
AI tool definition / tool call packages/spec/src/contracts/ai-service.ts AIToolDefinition, ToolCallPart none: name / toolName, no package member
MCP bridge packages/mcp/src/mcp-server-runtime.ts bridgeTools, registerToolFromDefinition none: server.registerTool(tool.name, …), execute({ toolName: tool.name }); the MCP SDK throws Tool NAME is already registered on a second name
agent guardrail packages/spec/src/ai/agent.zod.ts guardrails.blockedTopics none: exact match on the tool name
cloud chat (skill.tools) cloud packages/service-ai/src/skill-registry.ts#SkillRegistry.flattenToTools none: bare-name lookup in the tool registry (6725094995)
cloud MCP door, direct execute cloud service-ai-studio mcpDoorSource, tool-routes.ts none: the same bare-name ToolRegistry map (6725094995)

The card's rule: if any consumer reads by bare name, tool joins. Every one does, on both sides, so it joins. One nuance from the cloud reading, recorded in P1.10: cloud's runtime executes from its in-process ToolRegistry, filled by plugins in code, and mirrors those into tool metadata for Studio; a package-declared tool item reaches no handler there. So on cloud this refusal keeps /meta/tool/NAME (what Studio shows) and every bare-name reference unambiguous; it does not touch the registry's own last-wins set, which is cloud code (see Acceptance notes).

Responsibility (the claim asks which change let a second same-named tool install): no change regressed it. tool was never covered: ADR-0048 §3.4 retired the per-item cross-package throw for every type, and phase 1's ruling list (#30803) did not name tool, so PR #31375 (4a3b186c) recorded it as "not in the list" and pinned its coexistence as a CONTROL.

Epic #31527 / child R (#31575). This change needs nothing from R: it adds tool to phase 1's refusal, which the epic's red line keeps in force until a type's own retirement child. R's package-scoped resolution would later be what lets a type leave the list.

Verification

Two heads. The feature commits (ff31f6f9, 57fd18f8, 0c19dbb4: the last only the ADR note's text) were cut on base 69244406; main then moved 13 commits, one of them under packages/objectql/src/registry.ts (#31614), so origin/main was merged once at cbd8c5b2 (no conflict). Round 2 (30b6c84f, on top of it) changes only the ADR text, the anchor's invariant and the changeset's declaration line; no source or test file. The delta against origin/main is the 8 files above. The source-level runs below are on cbd8c5b2, whose source and tests are byte-identical to 30b6c84f; the gates and the three suites the seat asked for ran on 30b6c84f:

  • Unit, packages/objectql/src/registry-bare-name-one-holder-ai-family.test.ts: 38 passed (was 26). With the data, request and flow families' files and hook-binder.test.ts: 5 files / 192 passed, on 57fd18f8 and again on cbd8c5b2.
  • Dogfood, the four bare-name files (booted showcase + AppPlugin packages + MarketplaceInstallLocalPlugin): 4 files / 17 passed (ff31f6f9).
  • Ablation on 57fd18f8 (committed first; ablation-replace.mjs, marker ABL31414: tool filtered out of BARE_NAME_ONE_HOLDER_TYPES) → unit file 10 of 38 red (the list and declaredBareNames cells; for tool: the package door, the ABLATION cell's after-seal half, the item seam, the nested plugin, the cold-boot warn, reload/third-package and 'refuse' cells; the reason cell). The 28 green: every agent and skill cell, the tool's same-package reload, uninstall and environment CONTROLs, the four CONTROLs. objectql rebuilt with the mutation (ablation-dist-preflight: marker in 4 built files; DTS failed on the mutated literal's type, TS2322, after the JS bundles were written) → dogfood AI file 2 of 5 red: the cold-boot line did not name the tool "qa_shared_tool", and zeta installed (200). Restored (blob == HEAD, git diff HEAD empty), rebuilt, --absent: marker absent from all 14 built files, tree clean.
  • Consumer suites under os-verify-lock, every VERDICT command-exit 0. On 30b6c84f: metadata-protocol 327 passed, 4 skipped / 22593 passed, 26 skipped, rest 394 / 7546 passed, 332 skipped, runtime (--project local) 562 / 9021 passed, 19 skipped. On cbd8c5b2 (same source and tests): pnpm --filter @objectstack/objectql typecheck; objectql full suite 571 files / 11593 tests; mcp 60 / 915; pnpm --filter @objectstack/dogfood exec tsc --noEmit. (All five also green on the pre-merge 57fd18f8.)
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 30b6c84f: 90 derived families (the anchor file under scripts/ adds seven to cbd8c5b2's 83), each run with its exit captured before any pipe, all 90 exit 0. --ran: "90 derived famil(ies) accounted for — 90 run, 0 NOT-MEASURED" (exit 0). node scripts/check-adr-anchors.mjs exit 0. (The plugin-teardown self-test's pinned fixture was fetched first, git fetch --depth=1 origin 621a4876…, a shallow-clone prerequisite.)
  • eslint --no-inline-config over packages/objectql/src and the dogfood file: exit 0, no findings (neither package has a lint script).
  • Full dogfood battery (pnpm --filter @objectstack/dogfood test, under os-verify-lock) at cbd8c5b2: VERDICT command-exit 0 · Test Files 440 passed | 3 skipped (443) · Tests 3645 passed | 41 skipped (3686) · no failures. Also green on 57fd18f8 (440 | 3 files, 3642 | 41 tests). Ran because the title carries ! and the claim's line carries the (narrowing) arm; not re-run for 30b6c84f, which changes no file the battery reads.

Kept

  • Same-package re-registration, the environment catalog (ADR-0005 overlay), every agent / skill / data / request / flow pin green unchanged; rag_pipeline keeps §3.4 as P1.9 left it; the cold boot stays BARE_NAME_COLD_BOOT_MODE = 'warn'.

Acceptance notes

  • Cloud's ToolRegistry.register is a Map.set: a second code-registered tool under one name silently replaces the first (cloud reading 6725094995). That is cloud code, outside this card; carrier: cloud (the cloud seat files it, per seat 1).
  • Cloud mirrors code-registered tools into tool metadata only when absent (register('tool', …, { notify: false }), per the cloud reading). If that mirror ever registered package-bound after the cold-boot seal, a second plugin's same-named mirror would now be refused; the "when absent" guard means it never registers a name twice. NOT MEASURED against a running cloud build.
  • The addendum's own status line still reads "one family per change (P1.2, P1.7, P1.8, P1.9 — the last)": P1.10 is a dated note after the series, not a fifth family, and the ruling named four spots, so it is left as written.

Generated by Claude Code

Every reader of a tool names it with no package: /meta/tool/NAME serves
the first-registered package's tool, a skill's tools list admits only a
bare name, an AI tool definition and a tool call carry no package, and an
MCP server holds one tool per name. So `tool` joins the bare-name
one-holder list after the AI family: after the cold boot seals the
registry, the package door and the item seam refuse a second package's
tool under a held name, naming both holders and the rename; a boot that
already holds such a clash warns.

The AI family's unit and dogfood pins now carry `tool` through the same
matrix; ADR-0048 gains a dated note (P1.10) under its 2026-11-06 addendum.

Claude-Session: https://claude.ai/code/session_01ADHkmS1DVx9CH1xvXikQBW
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmS1DVx9CH1xvXikQBW
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/plugin.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/plugin.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 231 client-bound route-ledger rows — the other 166 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 166: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 107 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6c6cfcc1e271f593fe4a7919f12f855cc41e5bd5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8a2f0507eeb5be3fd6c7c99b32e0a071278b09f0 — the merge of head 30b6c84f2b0b9dff4da87be4be5c9f60b325514e into base 6c6cfcc1e271f593fe4a7919f12f855cc41e5bd5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8a2f0507eeb5be3fd6c7c99b32e0a071278b09f0 && git checkout 8a2f0507eeb5be3fd6c7c99b32e0a071278b09f0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6c6cfcc1e271f593fe4a7919f12f855cc41e5bd5 30b6c84f2b0b9dff4da87be4be5c9f60b325514e && git checkout -B drift-repro 6c6cfcc1e271f593fe4a7919f12f855cc41e5bd5 && git merge --no-ff 30b6c84f2b0b9dff4da87be4be5c9f60b325514e

node scripts/docs-audit/affected-docs.mjs --json 6c6cfcc1e271f593fe4a7919f12f855cc41e5bd5

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…nd P1.9; anchor invariant too

The narrowing is declared `no (narrowing)` in the changeset: nothing is
widened.

Claude-Session: https://claude.ai/code/session_01ADHkmS1DVx9CH1xvXikQBW
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

Contract-review-claim: session_01ADHkmPF1skfu2kx89BEXZi · head 30b6c84 · 2026-11-08T21:01Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 30b6c84f2b0b9dff4da87be4be5c9f60b325514e
Local-runs: none
Scope: docs/adr/0048-cross-package-metadata-collision.md + packages/objectql/src/bare-name-one-holder.ts, packages/objectql/src/registry.ts, packages/objectql/src/plugin.ts, packages/objectql/src/registry-bare-name-one-holder-ai-family.test.ts, packages/qa/dogfood/test/bare-name-one-holder-ai-family.dogfood.test.ts, scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json, .changeset/31414-bare-name-tool.md

Inputs: card #31414 (body + 6 comments, through 6726705019), PR #31661 (body, 8-file list, net diff vs merge-base c5361e2c), check-runs on the head (53), and the tree at origin/claude/project-thread-7w4hop read with git show only. Faces: rules (ADR-0048 is Tier H) and narrowing (Clause-②: no (narrowing)); on the narrowing face every non-prose path of the diff was read.

Check-runs on the head: 52 of 53 completed; every completed gate-carrying run is success (Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, Dogfood Regression Gate, Check Changeset ×2, Test Core 2/8–8/8, Live E2E UI, Test UI). Test Core (1/8) is still in_progress at review time, so the required Test Core set is not yet green; no run is red. The dogfood shards are skipped on the PR run (the queue leg carries them), so the booted pin in this diff has run only on the dev's side (reported 440 files / 3645 tests green at cbd8c5b2, same test sources as the head).

① Derived judgments

Every accept-set and public-surface change the diff implies, read from the tree and judged:

  1. Package door (SchemaRegistry.installPackage → refuseBareNameConflicts, registry.ts ~8719/4736). After the seal, a package whose manifest (or a nested plugins[] entry) carries tools[].name equal to a name another installed package holds is refused 422 NAMESPACE_CONFLICT ahead of every mutation. Mechanism: declaredBareNames iterates BARE_NAME_REGISTRY_TYPES (derived from the list, so it now includes tool) over COLLECTION_KEY.tool = 'tools', the same key the engine's METADATA_ARRAY_KEYS loop reads ('agents', 'tools', 'skills', engine-top-level.ts ~4087) and pluralToSingular('tools') === 'tool' (spec manifest-collection-spelling.ts:91). The door and the seam therefore see the same items under the same type. Right.
  2. Item seam (SchemaRegistry.registerItem, registry.ts ~7673). The guard is packageId && isBareNameRegistryType(type) && holdsBareName(type, item) && !isOverridableBaseline(type, packageId); isBareNameRegistryType('tool') is now true, holdsBareName is true for every non-action type, OVERRIDABLE_BASELINE_PACKAGES.tool is empty. So a package-bound registerItem('tool', …, 'name', pkg) after the seal over another package's name is refused before stamping or storing. A registration with no packageId (the environment slot, which is exactly how cloud's Studio mirror writes register('tool', …)) is never judged. Right.
  3. Cold boot (sealBareNameHolders, registry.ts ~4838; sealed by ObjectQLPlugin at kernel:bootstrapped, plugin.ts 1263–1274). The seal iterates BARE_NAME_ONE_HOLDER_TYPES, so a deployment already holding two tools under one name warns once (BARE_NAME_COLD_BOOT_MODE = 'warn' unchanged) naming both holders and the rename; which holder /meta/tool/NAME serves is unchanged (the dogfood pin asserts alpha). Right.
  4. Hook-binder function door. Untouched; tool is a registry type, not a function. Right (no change).
  5. Public surface. bare-name-one-holder.ts is not exported from packages/objectql/src/index.ts (grep: no hit), so the 'tool' member added to BareNameRegistryType and to the three Record<…> tables is internal vocabulary, not a public type widening. The public change is behavioural only: BareNameConflictError (already public via registry.ts) can now carry type: 'tool', and install-local can now answer 422 PLUGIN_REGISTER_FAILED for a tool. Right — a pure narrowing of the accept set; nothing widened.
  6. Stored data. No stored body changes shape; no conversion; nothing rewrites stored references. Right (matches the changeset's adr-0087: not-required).
  7. Other producers of package-bound tool items. Searched the tree for any registerItem('tool' / register('tool' outside the collection loop (ADR-0109's materialised action_<name> tools in particular): none exists in this repo; packages/mcp reads no metadata tool item (it bridges the AI service's code ToolRegistry). So the only package-bound producer the doors now judge is the manifest collection loop, in both its seams (manifest and nested plugin). Right — no unintended refusal path.
  8. Shipped producers. On the head no example app and no platform package declares tools: (hits: examples/app-showcase/scripts/mcp-fixture.mjs, an MCP fixture script, and the CLI generator's empty tools: []), so no existing deployment of this repo's packages gains a boot warning. Right (matches the changeset's "Measured producers"; HotCRM and marketplace packages correctly marked NOT MEASURED).

② Semver level

  • Changeset .changeset/31414-bare-name-tool.md: '@objectstack/objectql': minor, BREAKING banner, Clause-②: no (narrowing) and Metadata-protocol: narrowed both line-initial, exactly one ADR-0087 marker (<!-- adr-0087: not-required (no-migration-prescription) … -->), and the migration ("The one-line fix: rename the tool in one of the two packages — the refusal names the namespace-prefixed name — or uninstall one of them"). The PR body carries the same two declaration lines, line-initial.
  • Against AGENTS.md Post-Task Checklist step 3: the kind is narrowed ("a value refused or a requirement tightened"), and "a kind takes at least minor on a package the diff moves" → minor is the floor and is what is declared. (narrowing) is BREAKING and the banner is present; no (narrowing) is a well-formed form of the closed pair. Level matches what the diff publishes.
  • Clause-② criterion (does the card widen the accept set or the public surface?): ① items 1–5 show nothing widened — the only type-level growth is an unexported union — so the answer is no, arm (narrowing), BREAKING. Right, and consistent with feat(objectql)!: an agent and a skill hold one name per package (bare-name phase 1, 4/4) #31375's precedent for agent/skill. The dev's first spelling yes (narrowing) (from claim 6725057016) was corrected by the seat's re-stated claim 6726705019 and the head carries the corrected line in both places.
  • Metadata-protocol: narrowed — a package author's tools[].name is now refused under a held name after the seal: a requirement tightened. Right.
  • check:adr-0087-registration and Check Changeset are green on the head; the marker category reads as the gate's not-required arm with the written reason.

③ Boundary flags

Dev report 6726685498 (open_questions, deviations, out_of_scope_findings), against the seat's rulings in 6726705019:

  1. Q1 Clause-②: yes (narrowing) → keep or restate? Ruled B (restate no (narrowing)). The head's PR body and changeset both read Clause-②: no (narrowing). Answered and implemented.
  2. Q2 fold tool into the header Addenda line, §3.4's 2026-11-06 note, P1.5 and P1.9? Ruled B, with ⛔ never the Status line. The diff folds tool into exactly those four spots and adds P1.10; the Status line (ADR line 3) is byte-identical to origin/main (diffed the first four lines: identical), so draft PR docs(adr): ADR-0150 addresses behavioural metadata by (package, name); objects keep the literal prefix #31586's edit is not pre-empted. Answered and implemented. (The Addenda line is line 7, four lines below the Status line: no textual overlap with docs(adr): ADR-0150 addresses behavioural metadata by (package, name); objects keep the literal prefix #31586; a later rebase of whichever lands second stays mechanical.)
  3. Q3 update scripts/adr-anchors/packages__objectql__src__bare-name-one-holder.ts.json? Ruled B. The invariant gains one clause naming tool after the AI family; check-adr-anchors runs inside the green Lint & Repo Gates. Answered and implemented.
  4. Deviation: dogfood file outside packages/objectql/src/. The seat's re-stated claim widened the file surface to name the booted pin. Covered.
  5. Deviation: verification spans two heads (57fd18f8 / cbd8c5b2 / 30b6c84f). The check-runs on 30b6c84f are the verdicts here; see the in-progress Test Core (1/8) above. Answered by the head's check-runs, one still running.
  6. Deviation: first ablation marker restored before any run. No trace in the diff (net diff has no marker, no stray file). Closed.
  7. Deviation: PR assignee/reviewer unset. Per the standing order; the PR author is objectstack-fleet[bot] (relay on behalf of os-steve, as the body's attribution comment says). No note needed.
  8. Out-of-scope 1: cloud ToolRegistry.register is a Map.set (last wins). Named producer, carrier cloud; the cloud seat (6725094995) offered to file it. Escalated to the seat: confirm the cloud card exists — the dev's report records the offer, not a filed card. Not a finding on this diff.
  9. Out-of-scope 2: cloud's Studio mirror registering package-bound after the seal — NOT MEASURED. Recorded in the PR's Acceptance notes; by the cloud reading the mirror writes through register('tool', …) with no package, which ① item 2 shows is never judged, and only "when absent". Acceptable as noted (Prime Directive chore: version packages #10: noted, not buried).

④ Design judgment

The ADR against its first consumer.

  • P1.10 ↔ bare-name-one-holder.ts. "tool joins P1.2's list, last: the same two doors, the same holders, the same envelope and rename. It is not bundled, and it has no overridable baseline and no built-in name" — the list ends 'tool'; BUILT_IN_BARE_NAMES.tool and OVERRIDABLE_BASELINE_PACKAGES.tool are empty sets; the key is the plain pkg:name (no @member bundle). Sentence by sentence, the code agrees. The module doc's new tool bullet and CONSUMER_CLAUSE.tool ("a skill's tools and an MCP client name a tool") state the same measurement the note does.
  • P1.10 ↔ the two doors and the seal. Both doors are generic over the list (① items 1–3); the plugin seals at kernel:bootstrapped after kernel:ready's install-local rehydrate, as P1.3 says. The unit matrix (describe.each(['agent','skill','tool'])) pins for tool: package-door refusal with envelope/holders/rename and no record left, the ABLATION (pre-seal coexistence, post-seal refusal at both doors), the item seam, the nested plugin under its parent, same-package re-registration, uninstall releasing the name, the environment-slot CONTROL, the cold-boot warn, reload + third package, and 'refuse' mode. The booted pin drives the real install-local door (MarketplaceInstallLocalPlugin) and the real cold boot (two AppPlugins) and asserts /meta/tool/NAME keeps serving alpha. The three CONTROLs that replaced the old "tools coexist" CONTROL (different tool names; tool vs another package's skill/agent; one package's skill and tool under one name) are the right ones for "an agent, a skill and a tool are three types".
  • §3.4's 2026-11-06 note, P1.5, P1.9, header Addenda. Each now names tool with the date and points at P1.10; §3.4's "keeps §3.4 as written" list drops tool and keeps the object-bound action; P1.9's "Not in the list" is correctly re-scoped to "not covered by this change" with a forward pointer; P1.5 keeps "P1.9's change closes phase 1" and adds that tool joined afterwards on its own card. The ADR reads whole.
  • Execution plan and red lines. Bare-name phase 1: refuse a second package holding a non-object name whose consumers resolve without package context (ruling C on #30798) #30803 phase 1's list stays as ruled (P1.5 still says tool was never in the ruling's list); tool joins by the maintainer's acceptance of objectql: two packages' tools under one name, /meta/tool/NAME silently serves the first-registered one; measure cloud's by-name tool read, then decide whether tool joins the bare-name one-holder list #31414, quoted in P1.10. Nothing here builds package-scoped resolution (the doors refuse; no reader gains package context), and the refusal is the phase-1 kind the epic's red line keeps in force until a type's own retirement child — consistent with the PR's "needs nothing from child R". The cold boot stays 'warn', as P1.3 records the 2026-11-06 ruling.
  • Measurement. The card's rule was "if the by-name read has no package context, tool joins". /meta/tool/NAME alone satisfies it (P1.9 measured; the dogfood pin re-pins it on the head). The additional readers P1.10 cites were verified in the tree: SkillSchema.tools regex ^[a-z_][a-z0-9_]*\*?$ (skill.zod.ts:374), registerToolFromDefinition → server.registerTool(tool.name, …) / toolName: tool.name (mcp-server-runtime.ts 1262–1290), and AIToolDefinition/ToolCallPart carry a name only. Cloud's reading (6725094995) is quoted at its ref ba6f0fa.

Attempts to break it. (a) A second package-bound producer of tool items that the doors would now refuse unexpectedly: none exists (① item 7). (b) Divergence between what the door reads (COLLECTION_KEY.tool) and what the seam registers (METADATA_ARRAY_KEYS → pluralToSingular): both resolve tools → tool. (c) Cloud's mirror becoming a refused registration: it writes the environment slot, never judged. (d) An existing shipped clash turning a boot red: none declared; and the mode is 'warn'. (e) Status-line collision with #31586: byte-identical to main. None breaks.

Findings, graded:

  • nit — registry.ts ~8710, the installPackage door's comment still enumerates "a picklist, a mapping, a dataset, an API, an object-less action, a flow, a webhook, a connector, an email template" with no agent, skill or tool (the item-seam comment at ~7660 was updated; this one was not, and feat(objectql)!: an agent and a skill hold one name per package (bare-name phase 1, 4/4) #31375 left it the same way). Comment only; the code is generic over the list.
  • nit — The 2026-11-06 addendum's own status paragraph still reads "one family per change (P1.2, P1.7, P1.8, P1.9 — the last)" and does not mention P1.10; the header Addenda line, P1.5 and P1.9 do point at it, and the PR's acceptance note explains the choice (P1.10 is a dated note, not a fifth family; the ruling named four spots). No effect on the contract.
  • nit — P1.10's sentence "the MCP bridge registers each tool under its name and calls it by that name" is true of the reference protocol but could be read as the bridge reading metadata tool items; in this repo bridgeTools reads the AI service's code ToolRegistry, and the note's own cloud paragraph makes the mirror/handler distinction. Wording precision only.
  • escalation (not a finding on this diff) — the cloud ToolRegistry last-wins producer (③ item 8): the seat confirms a cloud card is filed.

No must-fix. No should-fix.

Implemented-by: claude/project-thread-7w4hop
Reviewed-by: session_01ADHkmPF1skfu2kx89BEXZi

VERDICT: PASS

Landing note (not part of the verdict): the diff touches docs/adr/**, so this is Tier H — the record certifies the contract; the landing still waits for an authorized APPROVED review and for every required check to finish (Test Core (1/8) was in_progress at review time), per Prime Directive #14.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

席位核对 · round 2 · head 30b6c84f · 2026-11-08T21:12Z

Verdict: ACCEPT. The seat adopts contract review record 6727183831 (Served-tier: CONTRACT_REVIEW_TIER, PASS, head 30b6c84f2b0b9dff4da87be4be5c9f60b325514e).

The seat read its Scope: line, which covers the ADR and every non-prose path of the diff (the narrowing floor). It also read the independence pair (Implemented-by: claude/project-thread-7w4hop, reviewer ≠ drafter) and the brief, which was read-only.

Landing: Tier H (docs/adr/**). The PR stays DRAFT. ⛔ No ready, auto-merge or queue until an authorized APPROVED review from GOVERNED_APPROVERS exists on the head. Review requested from os-zhuang.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 42539489585 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (1/8) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/validate-namespace-name.test.ts > ADR-0150 child N — os validate and os build refuse what the package door refuses, in its words > premise: the package
      ↳ 失败原因: @objectstack/cli:test: AssertionError: expected BareNameConflictError: Bare-name conflict… { …(8) } to be undefined
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • test/validate-namespace-name.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 11 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 9, 2026
@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 9, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 1f30be1 Nov 9, 2026
60 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-7w4hop branch November 9, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants