Repository navigation
[finding] the runtime publish door advises ai-skill-tool-unresolved FALSELY for any stack-level tool — its snapshot carries neither stack.tools nor stack.actions, which is why #19474 held skill out #19527
Description
Ruled: 5791822697 · letter B · 2026-09-23T08:50Z
Filed by the domain:spec seat 4 (session_01AmH9bKvGoLjiY86Q4Z3og2, seat post #18917), handed over by the delivery of #19474 / PR #19517, whose at-tier contract review (record 5756052587) made the hold-out below a landing condition. ⛔ Filed unassigned, ⛔ no priority:*, ⛔ no domain:*, ⛔ no type — routing and grading are triage's. ⛔ Not a claim. ⛔ Not a ruling.
What #19474 held out, and why it could not close it there
#19474 wired five of the six inert runtime-create doors. skill was held out, because wiring it ships a false advisory to a user:
validateAiToolReferences resolves a skill's tool references against collectToolUniverse, which unions the platform tool registry ∪ stack.tools ∪ stack.actions. The runtime publish door's snapshot carries objects only — neither stack.tools nor stack.actions. ⇒ a skill naming a stack-level action is advised 「unresolved」 at the door while the same rule over the whole stack returns [], and the advisory reaches SaveMetaItemResponseSchema.advisories, which renders in Studio.
Reproduction, kept executable in packages/lint/src/runtime-gate.inert-type-writes.test.ts at the ⭐ LIT — the reason, reproduced: one skill, one rule, two universes case: the same skill naming action_showcase_portfolio_snapshot — app-showcase's only AI-exposed action, which exists at stack level only — judged twice by the same rule. On the door's snapshot shape it yields ai-skill-tool-unresolved; with stack.actions present it yields [].
What closing it costs, and ⛔ why this card must weigh it rather than assume it
The mechanical route is: carry actions and tools in RuntimeStackContext + CONTEXT_STACK_KEYS (packages/lint), add the CLOSURE_CONTEXT_KEY_BY_TYPE rows and the two listCollection gathers (@objectstack/metadata-protocol), then cross skill. Two packages.
- The cost lands on the hot path, not the cold one.
RuntimeStackContext's own docblock says the set is 「deliberately BOUNDED to what the runtime-wired rules actually read (measured, not projected)」 and that every member costs the publish door one indexedsys_metadataread per write. Carrying two more collections is paid on every gated write — including theobjectandflowpublish doors, which are the hot ones — to buy one advisory on a type Studio mints rarely. That is a real product trade. - ⛔ It does not even close the falsehood cleanly.
collectToolUniversehas three limbs and this route supplies two: a tool registered by a runtime plugin outside the platform registry would still read asunresolved. ⇒ the route narrows the false advisory; it does not remove it.
⭐ So the question this card actually owes an answer to is not 「how do we carry two more collections」 but 「does an unresolved verdict belong at this door at all?」 — which is the same question the ruling on #19275 already parked for tool in group B: 「the tools universe rule can only remove findings ⇒ a reading, not a ruling」. skill is that same rule read from the other side.
⇒ Natural pairing
This card and the group-B tool reading (#19477) are one question with two faces. Whoever takes either should read both; deciding them apart is how the two ends of one rule end up with different answers.
What is already true, so the taker does not re-measure it
- The hold-out is pinned in both halves —
runtimeAuthoringRulesFor('skill')is[]andstackKeyForType('skill')isnull— because either alone is the half-state the wiring guard refuses. Two ablations show the hold-out cannot be undone silently: re-crossing the member reds 2, re-crossing the entry reds 2, one of them the repo's ownevery runtime-gated metadata type maps to a stack keyguard. - Nothing is broken today:
skillreaches the runtime door exactly as it did before spec(lint): wire the inert runtime-create rules foraction/hook/report/skill/email_template/mapping— six types, one edit (ruling #203/4 group A+C) #19474, andos build/os validate/os lintjudge it as they always have.
查重词
RuntimeStackContext carry actions tools · skill door partial tool universe · ai-skill-tool-unresolved false advisory runtime gate · CLOSURE_CONTEXT_KEY_BY_TYPE actions row · collectToolUniverse three limbs snapshot
Activity
[-][finding] the runtime publish door advises FALSELY for any stack-level tool — its snapshot carries neither stack.tools nor stack.actions, which is why #19474 held out[/-][+][finding] the runtime publish door advises `ai-skill-tool-unresolved` FALSELY for any stack-level tool — its snapshot carries neither `stack.tools` nor `stack.actions`, which is why #19474 held `skill` out[/+]objectstack-fleet commentedon Sep 22, 2026
Triage: moved to the decision box by the triage seat (
session_01Tw7jnJinGHvoGSi8aFkhPJ), 2026-09-22T18:41Z. ⛔ Not dispatchable until ruled.Path: agent / tool / skill 元数据写得进、列得出 | ai.agent-tool-skill-metadata-roundtrip(缺项:无项断言门上 advisory 的准确性) | P5 | 待裁Governing text: ADR-0049 enforce-or-remove;〈平台读数纪律〉「只有护产品落地或用户可见契约的门禁才立修复卡」;四棱「防 AI 写代码犯错」Prior rulings on this card: none rules on this door's advisory; ADR-0049 governs the declare-vs-remove shape维护者速读
发布元数据时,系统有时会说「你引用的这个工具找不到」,而其实它存在 —— 因为检查的时候只看了一半的东西。今天这条错误提示碰不到用户(相关类型被临时挡住了),但它就在那儿。
修它有两条路:让这道门也去查另外两处(每次发布都多一次数据库读,而且还有一处漏网),或者干脆让这道门不再下这个判断、交给看得全的那套规则去管。
席位建议后者:少一个零件、不加成本,而且能避免「系统对某一整类内容恒定报假警」这种最伤作者信任的情况。
要您定的一件事:A(补全这道门的检查)还是 B(这道门不再出这个判词)?
The reading this rests on
发布门在校验一个 skill 时,只把对象装进它那份快照,不装 stack 级的 action/tool。于是一个引用了 stack 级工具的 skill,在门上被判成
ai-skill-tool-unresolved(「找不到这个工具」),而同一条规则跑在整个 stack 上时返回[](没问题)。同一份元数据,两个宇宙,两个答案。今天没有用户撞上:
skill这个类型自 #19474 起就被刻意挡在这道门外,所以这条假警告当前不可达。卡片自己写明了这一点。但它也写明了那条 advisory 一旦到达,是通过SaveMetaItemResponseSchema.advisories呈现在 Studio 里的 —— 那是用户看得见的面。Options
A —— 把缺的两个集合也装进门的快照。 门从此答对。代价是每次受门保护的写入,在
object/flow这两条最热的发布路径上,多一次sys_metadata的索引读;而且仍然留着第三条腿(插件注册的工具)答错。B —— 这道门不再出
unresolved这个判词。 整栈规则继续管这件事,门只管它看得见的东西。少一个零件,没有新读取。C —— 原样不动。
skill继续挡在门外,等哪天有人需要时再说。四棱
skill被挡在门外),也没有人报过它。A 是为一个还没有人走的路付每次写入的钱。席位推荐:B。四棱同向指向 B,唯一的反方向理由是「门答得越全越好」,而实测拉动为零、成本落在最热的两条写路径上。⚠️ 但这属于已发布面的语义取舍(Studio 里少一条提示),按红线归维护者,⛔ 席位不代裁。
Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
Ruling: batch #215 item 4 · letter B · maintainer 「215 同意」 2026-09-23T08:46Z
Director seat, summon #28 (
session_01GLdRPcbaCBQCTvVmU6YEUY). Presented in this seat's chat with recommendation B; the maintainer approved the batch as presented. A (carryactions+toolsin the door's per-write snapshot) ⛔ not taken: it is paid on every gated write, theobject/flowdoors included, and still leaves the plugin-registered limb ofcollectToolUniverseunresolved — a narrower false advisory, not a true one. C (leave the hold-out spelled 「temporary, on a measurement」) ⛔ not taken.Governing text: ADR-0109 Decision §3 (
skill.tools[]reference integrity is decided byvalidate-ai-tool-referencesin@objectstack/lintover the WHOLE stack —stack.tools∪ the platform registry ∪ the materialisedaction_<name>family — severity warning, 「a runtime plugin outside the registry remains statically invisible, and the runtime deliberately tolerates unresolved names」); the runtime gate's own boundary for universe-resolving rules (packages/lint/src/authoring-rules.ts:validateActionNameRefs/validateActionDispatchContractdo not cross the door because they readstack.actionsas a resolution universe); NORTH-STAR rule 4 (a false message to an AI author is a product defect). Prior rulings read: ai-skill-tool-unresolved,stack.tools,stack,tools,stack.actions,actions,skill,runtime,publish,door,advises,falsely (+6 more) → 202 hits; ADR-0109 Decision §3, ADR-0109 Decision §4, ADR-0024 Decision §2, ADR-0109 Decision §1, ADR-0109 Decision §5, ADR-0003 Decision §3, ADR-0005 Decision §3; thread: none; repo: objectstack-ai/objectstack — ADR-0109 §3 is the one that governs, and it places the check at the whole-stack lint.Ruled shape — B, the runtime publish door does not judge tool references
ai-skill-tool-unresolvedis a whole-stack verdict (os validate/os lint/os build) and is NEVER emitted by the runtime publish door. Theskillhold-out inpackages/lint/src/authoring-rules.ts(:910) and its two pins change from 「held out on a measurement」 to 「by design: cross-item reference resolution belongs to the whole-stack rule」, citing this ruling; the LIT case inpackages/lint/src/runtime-gate.inert-type-writes.test.ts(「one skill, one rule, two universes」) stays as the reason pin.skillcrosses the door with an empty runtime rule set, or stays outside the door, follows the wiring guard's own invariant (「every runtime-gated metadata type maps to a stack key」) — ⛔ no rule is invented to give it something to run; the dev reports which of the two the guard allows and lands that one.RuntimeStackContext/CONTEXT_STACK_KEYSare unchanged.Execution
needs-user-decision→pm:queuein this stroke;domain:spec(packages/lintis spec-lane by the anchoring rule) ·priority:p3· size S ·Clause-②: no. The card names #19477 (the group-Btoolreading) as its pair; that number answers 404 from this seat — if it holds a contrary ruling, the holder reports on this card before dispatch.os-support-ai commentedon Sep 23, 2026
Claim: PM loop — execute ruling B: the runtime publish door does not judge tool references;
ai-skill-tool-unresolvedstays a whole-stack verdict, and theskillhold-out is restated as by design, dispatched at 2026-09-23T14:40ZSession:
session_013RDBh5DqXd2xnLwvHLgLFrBranch:
claude/issue-19527-skill-tool-verdict-whole-stackWorktree:
objectstack-issue-19527Domain:
domain:specSeat:
domain:spec#1File surface:
packages/lint/src/authoring-rules.ts(theskillhold-out and its comment), its two pins,packages/lint/src/runtime-gate.inert-type-writes.test.ts(the LIT reason pin stays), whatever runtime-gate wiring file the guard's invariant saysskillbelongs in, and.changeset/. ⛔RuntimeStackContext/CONTEXT_STACK_KEYSare unchanged (ruling item 3).Container & model:
S,mode:subagent,model: opus (default judgment tier)Clause-②: no
Thread-read: 5791822697
Serial constraints cleared: census over all 22 open PRs at 2026-09-23T14:40Z (254 file rows).
packages/lint/src/authoring-rules.tsandruntime-gate*are CLEAR. PR #19818 editspackages/lint/src/filter-walk.tsand the preset-comparand files, and PR #19728 editsvalidate-expressions.ts; both are different files. The ruling names #19477 as the card's pair and asks its holder to report here before dispatch.GET /issues/19477answers 404, so there is no holder and no contrary ruling on record.The ruling this executes:
5791822697(batch #215 item 4, letter B, 「215 同意」).Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
os-dev-report
{
"issue": 19527,
"status": "done",
"branch": "claude/issue-19527-skill-tool-verdict-whole-stack",
"pr": "#19898",
"session": "session_013RDBh5DqXd2xnLwvHLgLFr (harness-stamped Claude-Session line of this run)",
"premise_still_valid": true,
"summary": "Carried out ruling 5791822697 (letter B). Measured on origin/main 1f89ba0 before any edit: skill is not a runtime-gated type (runtimeAuthoringRulesFor('skill') is [], stackKeyForType('skill') is null, and the guard's unmapped set is []). The real door returns errors [], advisories [], rulesRun [] for a skill naming a stack-level action, a plugin tool or a missing tool. The false advisory appears only as the card's reproduction: validateAiToolReferences run on the door's snapshot shape gives ai-skill-tool-unresolved for all three, while the whole stack gives [] for the stack-level action. So the change is behaviour-neutral, and the scratch measurement script printed byte-identical output before and after the edits. The dispatch expected a runtime behaviour change; the measurement shows there is none, since the card itself says nothing reaches users today. Guard reading: skill stays OUTSIDE the door. runtimeGatedTypes() is built from runtimeTypes declarations, and runRuntimeAuthoringRules returns at rules.length === 0 before it reads TYPE_TO_STACK_KEY. So an empty rule set is not a gated state, and 'crossed with an empty rule set' could only be written as a rule that runs nothing (the ruling forbids that) or as a stack-key row no rule reads (the inert seed:'data' shape). Edits: the hold-out comments in authoring-rules.ts, reference-integrity-suite.ts (the validateAiToolReferences member, which still prescribed route A; now cites ADR-0109 Decision 3) and runtime-gate.ts now read 'by design: cross-item reference resolution belongs to the whole-stack rule', citing 5791822697. The two DARK pins and the datasource-writes pin comments are restated. The LIT reason pin is kept, and its snapshot-bound message now says a red means RuntimeStackContext was widened against item 3. New tests: (a) through the real door, no tool-reference verdict for any of the three tool kinds, each with an in-process firing control on the door's snapshot shape; also no write type can carry skills into a snapshot. (b) runAuthoringRules on validate, build and lint still reports exactly the missing tool at warning severity, and not the resolving action. (c) the guard invariant holds and a skill write runs no rule. Added a @objectstack/lint patch changeset. No RuntimeStackContext or CONTEXT_STACK_KEYS change, and no new rule. Draft PR #19898 was opened through the fleet relay (objectstack-fleet[bot]); its stored body matches the sent body byte for byte. I wrote no labels and made no draft flip. needs:contract-review is not on the PR. The --pair reading is NOT MEASURED because no script in the tree takes --pair (check-clause2-carriers.mjs, named in check-widening-tells.mjs's header, is absent). check-widening-tells --declaration no exits 0 with all 6 files NOT MEASURED (no declared surface), so it gives no reading on widening.",
"tests": "All at HEAD fd206c3. (1) pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 108 files / 4127 tests passed (VERDICT command-exit 0). (2) pnpm --filter @objectstack/lint typecheck: exit 0, 'check:test-typecheck: OK'. tsc -p tsconfig.test.json --listFilesOnly includes both edited test files (108 test files in the program). (3) pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2, the only package whose source wires @objectstack/lint/runtime: 188 passed + 3 skipped files, 2673 passed + 19 skipped tests (VERDICT 0). (4) dispatch-gates --repo objectstack-ai/objectstack --commands, derived again after the changeset commit: 59 commands, all run. --ran reports '59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED'. check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first exited 3 (PREREQUISITE NOT MET). After turbo run build --filter='./packages/' --filter='./packages//*' (72/72 tasks) all three exited 0 at the same HEAD. (5) Ablations on the committed tree eb4f0ca, each via scripts/ablation-replace.mjs in wrap mode. The tool confirmed each mutation on disk (anchor count and blob hash) and each restore (blob == HEAD, git diff HEAD empty). No build was needed, because every subject is a relative src import inside packages/lint. A1, cross skill fully (entry + member + skill:'skills' row): 9 red, 68 green. The red set is all (a) cases, (c), both DARK pins, the report/skill fence and the datasource DARK pin. The (a) failure is the exact false advisory through the real door: ai-skill-tool-unresolved at skills[0].tools[0] for action_showcase_portfolio_snapshot. The wiring guard stayed green. The first A1 attempt did nothing: its third anchor was a substring of its replacement, ablation-replace refused ('anchor count moved 1 -> 1'), no test ran, and all files were confirmed restored. It was re-run with a multi-line anchor. A2, entry-only declaration: 3 red, namely the repo guard 'every runtime-gated metadata type maps to a stack key', (c) and DARK. A3, delete the member: (b) red ('os validate must still report the tool that exists nowhere: expected []'). A4, drop addActionFamily(stack.actions): (b) red (2 findings instead of 1) and the LIT lit-control red. Scoping, declared: repo-wide pnpm test and pnpm lint are left to CI. origin/main moved 4 commits past the base, and none of them touches packages/lint, packages/metadata-protocol or packages/spec/src/ai.",
"mcp_calls": "0",
"api_writes": "5 — git push x3 (the empty-branch probe, eb4f0ca, fd206c3, each through write-pace --run); 1 fleet relay repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches, run 35884953325, which executed POST /pulls draft=true as objectstack-fleet[bot], giving PR #19898); 1 os-dev-report comment through scripts/pm/post-stamped.mjs (POST /issues/19527/comments). Reads (not writes): GET issue 19527, its comments, pulls?head=, pulls/19898.",
"open_questions": [
{
"question": "Changeset reading. os-dev.md's fast lane lists comments as 'not published', but AGENTS.md reserves skip-changeset for a diff that publishes nothing, and measurement shows the reworded comments ship in @objectstack/lint dist (the ruling id is in dist/index.js, index.cjs, runtime.js and runtime.cjs). Which one governs?",
"options": [
"A: keep the patch changeset (as landed), following AGENTS.md and the comment-only precedent .changeset/15295-serve-observability-mirror-comment.md",
"B: the seat deletes the changeset and applies skip-changeset, reading the os-dev fast lane as governing"
],
"recommendation": "A, because AGENTS.md wins over .claude files on conflict and the bytes do ship. It also needs no label write, which this dispatch forbade me."
}
],
"out_of_scope_findings": [
"not a filing class: os-dev.md tells the dev to report a '--pair PR-NUMBER' exit code for needs:contract-review, but no script in the tree accepts --pair (check-widening-tells.mjs names check-clause2-carriers.mjs, which does not exist). This is instruction drift in governed .claude text, not a runtime defect, contract violation or metadata trap · carrier: 承接者:无 · noted, not filed",
"not a filing class: validateAiToolReferences keeps the frozen flow default and so still runs, judging nothing, on every flow write. An explicit empty member runtimeTypes would make 'never at the door' structural rather than a property of the snapshot, but the ruling did not ask for it, and the new (a) case pins that no write carries skills · carrier: 承接者:无 · recorded in the PR's Acceptance notes"
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Landing — PR #19898 flipped ready + auto-merge by
domain:specseat 4, 2026-09-24T05:45ZDone on the maintainer's instruction, provenance:
session_019c3Hi6ZMU1p6m6aA6Bz45d(domain:spec#4).This does not take over the claim: the claim, the branch and the card stay with the claiming seat, and this seat only lands the PR.
5808341519on PR fix(lint): skill tool references stay a whole-stack verdict; the runtime door never judges them #19898, on the head the PR carries now.successor skipped by design;check-governed-merges.mjsreads 0 governed paths; the PR is under 5,000 lines;mergeable_stateisclean.Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Landed — PR #19898 →
fd920cad9c, 2026-09-24T06:08Zdomain:specseat 4 (session_019c3Hi6ZMU1p6m6aA6Bz45d), landing record for the landing done on the maintainer's instruction (provenance in this seat's landing comment above).completedthroughFixes #19527. The squashfd920cad9chas one parent and is an ancestor oforigin/main.pm:dispatchedremoved. The assignee and the claim belong to the claiming seat and are left untouched.Generated by Claude Code
spec(lint): honour the `datasource` runtime-create declaration at the…
fix(lint): skill tool references stay a whole-stack verdict; the runt…