Skip to content

[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

@os-steve

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.

⚠️ Two things this card must decide, ⛔ not inherit:

  1. 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 indexed sys_metadata read per write. Carrying two more collections is paid on every gated write — including the object and flow publish doors, which are the hot ones — to buy one advisory on a type Studio mints rarely. That is a real product trade.
  2. ⛔ It does not even close the falsehood cleanly. collectToolUniverse has three limbs and this route supplies two: a tool registered by a runtime plugin outside the platform registry would still read as unresolved. ⇒ 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

查重词

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

  1. changed the title [-][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[/+] on Sep 21, 2026
  2. objectstack-fleet commented on Sep 22, 2026

    @objectstack-fleet
    Contributor

    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 让「谁有资格下这个判词」变清楚:看得全的那个才判。A 把两个宇宙拉齐了一半(还剩插件腿),等于把同一个病根留在原地。
    • 防 AI 写错:这条最重要,而且它指向 B。一条对整整一类元数据恒假的警告,教会作者忽略警告面 —— 比没有警告更糟。A 也能消除假警告,但它留下的第三条腿意味着假警告还会再来一次。
    • 创业阶段不扩散:B 是删零件,A 是加零件并加每次写入的成本。阶段姿态默认从紧。

    席位推荐:B。四棱同向指向 B,唯一的反方向理由是「门答得越全越好」,而实测拉动为零、成本落在最热的两条写路径上。⚠️ 但这属于已发布面的语义取舍(Studio 里少一条提示),按红线归维护者,⛔ 席位不代裁。


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    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 (carry actions + tools in the door's per-write snapshot) ⛔ not taken: it is paid on every gated write, the object / flow doors included, and still leaves the plugin-registered limb of collectToolUniverse unresolved — 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 by validate-ai-tool-references in @objectstack/lint over the WHOLE stack — stack.tools ∪ the platform registry ∪ the materialised action_<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 / validateActionDispatchContract do not cross the door because they read stack.actions as 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

    1. ai-skill-tool-unresolved is a whole-stack verdict (os validate / os lint / os build) and is NEVER emitted by the runtime publish door. The skill hold-out in packages/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 in packages/lint/src/runtime-gate.inert-type-writes.test.ts (「one skill, one rule, two universes」) stays as the reason pin.
    2. Whether skill crosses 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.
    3. ⛔ No snapshot widening: RuntimeStackContext / CONTEXT_STACK_KEYS are unchanged.

    Execution

    needs-user-decision → pm:queue in this stroke; domain:spec (packages/lint is spec-lane by the anchoring rule) · priority:p3 · size S · Clause-②: no. The card names #19477 (the group-B tool reading) as its pair; that number answers 404 from this seat — if it holds a contrary ruling, the holder reports on this card before dispatch.

  4. os-support-ai commented on Sep 23, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop — execute ruling B: the runtime publish door does not judge tool references; ai-skill-tool-unresolved stays a whole-stack verdict, and the skill hold-out is restated as by design, dispatched at 2026-09-23T14:40Z
    Session: session_013RDBh5DqXd2xnLwvHLgLFr
    Branch: claude/issue-19527-skill-tool-verdict-whole-stack
    Worktree: objectstack-issue-19527
    Domain: domain:spec
    Seat: domain:spec#1
    File surface: packages/lint/src/authoring-rules.ts (the skill hold-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 says skill belongs in, and .changeset/. ⛔ RuntimeStackContext / CONTEXT_STACK_KEYS are 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.ts and runtime-gate* are CLEAR. PR #19818 edits packages/lint/src/filter-walk.ts and the preset-comparand files, and PR #19728 edits validate-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/19477 answers 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

  5. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    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

  6. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Landing — PR #19898 flipped ready + auto-merge by domain:spec seat 4, 2026-09-24T05:45Z

    Done on the maintainer's instruction, provenance:

    • who: the maintainer;
    • words: 「帮我处理」 for a list of twelve PRs "只差一份 at-tier 复核的 PASS 记录", then the landing route 「我直接落地」 chosen in the same exchange;
    • where: the chat of session 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.

    • Contract review: at-tier PASS, record 5808341519 on 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.
    • Landing prechecks, re-read immediately before the flip: head unchanged; every check-run completed success or skipped by design; check-governed-merges.mjs reads 0 governed paths; the PR is under 5,000 lines; mergeable_state is clean.
    • The accepted gap is named in the record (③): runtime-mint paths reach no whole-stack verdict, which is the outcome ruling B chose.

    Generated by Claude Code

  7. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Landed — PR #19898 → fd920cad9c, 2026-09-24T06:08Z

    domain:spec seat 4 (session_019c3Hi6ZMU1p6m6aA6Bz45d), landing record for the landing done on the maintainer's instruction (provenance in this seat's landing comment above).

    • The card closed completed through Fixes #19527. The squash fd920cad9c has one parent and is an ancestor of origin/main.
    • Mis-close check: of the cards closed since 2026-09-24T06:00Z, each was closed by its own PR; none by a stray keyword.
    • pm:dispatched removed. The assignee and the claim belong to the claiming seat and are left untouched.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:aiAI-native — agent / tool / skill metadata, and the MCP surface an agent drivesdomain:specpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions