Repository navigation
Description
Split out of #3583, which is otherwise closed: eight of its nine bug categories are now gated on validate/lint/compile, and the ninth (hook body write sets) is an accepted, documented gap. This is the one category with resolvable references and no rule — assessment R7 in docs/audits/2026-07-app-metadata-reference-integrity-assessment.md §3.
The gap
packages/lint/src contains zero AI rules. Nothing checks that:
agent.skills[]names a skill the stack declares (ai/agent.zod.ts)skill.tools[]— wildcard-aware — names a declared tool (ai/skill.zod.ts)agent.tools[].nameresolves to an action/flow by its declared type
Evidence on the real corpus (2026-07-28, hotcrm @ 8b28fa2, v2.2.2)
| Surface | Count |
|---|---|
| agents | 2 |
| skills | 6 |
agent.skills references |
8 |
skill.tools references |
16 |
| tools declared by the stack | 0 |
So all 16 skill→tool references are dead today — the original audit's "agent skills listing 11 tools with no definitions", still shipping. Every one passes objectstack validate and objectstack lint cleanly.
Two decisions come first (neither is a lint question)
- D2 — runtime-registered kernel skills/tools vs. static lint. The
ask/buildkernel agents register their skills/tools via service at boot, so a stack can legitimately reference names no static analysis can see (same invisibility as plugin objects, which feat(lint): reference-integrity validation for object and action names (#3583) #3657 solved with the curatedPLATFORM_PROVIDED_OBJECT_NAMESregistry). Either extend that precedent with official skill/tool names, or run R7 at advisory severity. Small; take it when the rule starts. - D1 —
agent.knowledge.indexeshas no definition site.KnowledgeSourceSchemais never referenced bystack.zod.ts, so the namespace is unresolvable by construction — parsed, unmarked, and unenforceable, which is the state ADR-0049's trichotomy prohibits. Either add aknowledgeSourcesslot to the stack (spec change, ADR-worthy) or mark the fields[EXPERIMENTAL — not enforced]. Linting a namespace with no definition site would institutionalise the gap, so the knowledge half of R7 stays blocked until this lands. HotCRM carries 2 such references.
Out of scope
The "hand-off to a nonexistent skill" instance from the original audit: hand-off has no schema field anywhere — it lives in free-form prompt text, which has no structure to check (§7 non-goal).
Done looks like
validate-ai-references in @objectstack/lint, appended to REFERENCE_INTEGRITY_RULES (one line — validate/lint/compile pick it up automatically), with fixtures from the HotCRM instances and a zero-false-positive pass over examples/. Note the verification lesson from #3806: the false-positive floor must be read per branch — an example corpus that declares no agents proves nothing about the agent branch.
Refs #3583 · ADR-0049 · ADR-0072 · ADR-0078
Activity
refactor(lint): one reference-integrity entry point + the HotCRM resi…
os-zhuang commentedon Jul 28, 2026
Positioning check before D1/D2: the agent branch lints a surface ADR-0063 closed, and the tool branch has no registry to resolve against
Read of the three repos at today's HEAD (
objectstack@87aca93,cloud,hotcrm). The conclusion is that R7 should not be built as specced — not because the references don't matter, but because two of its three branches are checking namespaces whose authoring status is itself unresolved. There is a decision upstream of D1/D2.1. Agents are already a closed surface — but only half-closed, and not at the authoring layer
ADR-0063 §2 decided it: "third parties author
*.skill.tsand tools / MCP connectors.*.agent.tsis closed to third parties", and ADR-0040 §3 (tenant custom agents) is withdrawn. That landed in two places:spec/src/kernel/metadata-plugin.zod.ts:697—agentcarriesallowRuntimeCreate: false, allowOrgOverride: false.cloud/packages/service-ai/src/agent-runtime.ts:199-208—listAgents()filters the catalog to platform-owned records.It did not land at the authoring layer, and the withdrawal has holes:
stack.agentsslotspec/src/stack.zod.ts:303stack.zod.ts:295-298docdefaultAgentis selected automatically")app.defaultAgentui/app.zod.ts:675), still an editable field inui/app.form.ts:39, and its doc example is literallydefaultAgent: 'sales_copilot'— the withdrawn patternloadAgent()(chat route)agent-runtime.ts:244resolveDefaultAgent()app.defaultAgentand loads it unfiltered —agent-runtime.ts:496-502So a stack-authored agent today is hidden from the picker but still reachable — via
POST /agents/:name/chatand viaapp.defaultAgent. HotCRM's two agents are exactly that state: parsed, built into the artifact, invisible, reachable.This is the ADR-0078 prohibited state, and the remedy ADR-0078 prescribes is not a lint rule about the slot's contents — it is closing or marking the slot. Writing
validate-ai-referencesoveragent.skills[]would institutionalise an authoring surface the platform decided not to have — the same objection the issue itself raises for the knowledge half in D1.Corroborating: on the motivating corpus the agent branch finds nothing. HotCRM's 8
agent.skillsreferences (sales_copilot5,service_copilot3) all resolve against its 6 declared skills. 8/8 clean.2.
stack.toolshas zero readers — the tool half cannot resolve because there is no tool authoring story yetSearched both repos for any read of the
toolmetadata type:One write, no reads. The plugin's own comment says it: "Studio visibility: persist tool/agent/skill metadata… nothing subscribes to
tool/agent/skillevents" (plugin.ts:518-524). The executable path is the in-processToolRegistry(aiService.toolRegistry.getAll(),routes/agent-routes.ts:582), which the metadata service never feeds.Consequences:
tooldeclared in an app stack registers a record, never a callable —ToolSchemais pure declaration with no handler binding. "Third parties author tools" (ADR-0063 §2) is not currently possible; today the real third-party primitive is skills alone, and a skill'stools[]can only name runtime-registered tools it does not own.'tools'is missing fromCONCAT_ARRAY_FIELDS(stack.zod.ts:1161-1188, which does list'agents'and'skills'), socomposeStackssilently drops it — consistent with having no consumer.plugin.ts:527-531skips registrationif (exists), so a tenant-declaredtoolof the same name would suppress the platform's own metadata mirror. Shared namespace, no prefixing.3. Re-measuring the corpus: the "all 16 dead" figure is an artifact of resolving against the wrong registry
The issue reads 16 dead
skill.toolsreferences because the stack declares 0 tools. Resolved against the registry that actually decides the call — the in-process one — the split is:describe_object,list_objects,query_records,get_record,aggregate_data,search_knowledge)triage_case,generate_email_copy,optimize_subject_line,personalize_content,generate_email,analyze_lead,suggest_next_action,analyze_pipeline,identify_at_risk,forecast_revenue)A rule resolving against
stack.toolsalone would therefore emit 6 false positives out of 16 (37.5%) on its own motivating corpus — a rate that gets a rule disabled rather than fixed. And the runtime treats the miss as tolerable by design: "Tools that are declared by a skill but missing from the available tool registry are silently dropped — this is intentional so a skill can be authored before all its underlying tools are registered" (skill-registry.ts:196-199, same wording atagent-runtime.ts:376-379).Also worth noting for the verification plan:
examples/declares zero agents, skills and tools (grep -rl "defineSkill|defineAgent|defineTool" examples/→ 0 files). Per the #3806 lesson the issue already cites, the false-positive floor for R7 is unmeasurable onexamples/— every branch, not just the agent one.4. What is actually worth building — and it is not in R7's list
resolveActiveSkills()throws on a skill↔agent surface mismatch (agent-runtime.ts:468-477, ADR-0064 §3). That is a statically decidable, runtime-fatal condition surfacing as a 500 at chat time. Zero-false-positive, cheap, and it protects the invariant that keepsaskfrom authoring. This is the AI rule with the best evidence behind it, and the issue does not mention it.Two adjacent findings from the same read, offered as separate work:
agent.tools[]seam.buildRequestOptions()resolvesagent.tools[].nameagainst the full registry passed in (agent-runtime.ts:409-424), with no surface check — while the route passestoolRegistry.getAll(), which includes AI Studio's authoring tools when that plugin is loaded. Combined with §1's unfilteredloadAgent/app.defaultAgent, a stack-authoredsurface:'ask'agent can name build-surface tools. Whether that is exploitable depends on the per-tool authz interceptors, which I did not audit — but the scoping invariant ADR-0064 states is not enforced here.skill.permissionsis silently stripped. HotCRM'slive_dataskill declarespermissions: ['crm:read'](hotcrm/src/skills/live-data.skill.ts);SkillSchemahas no such field, so Zod drops it. A permission-shaped declaration that does nothing — ADR-0049 territory.5. Proposed disposition
Insert D0 ahead of D1/D2: decide whether
toolis a third-party-authorable metadata type or in-process registration only. Until that is answered,stack.toolsis inert metadata and no reference rule over it can be correct in either direction. It is also the decision that determines whether ADR-0063 §2's "skills and tools" is a shipped contract or an aspiration.Then, in dependency order:
stack.agentsto app packages, or mark it[EXPERIMENTAL — INTERNAL]per ADR-0078; fix the unfilteredloadAgent/resolveDefaultAgentpaths; retire or re-documentapp.defaultAgentand the stalestack.zod.ts:295-298prose. HotCRM's two agents then get deleted or become legitimate — either way the R7 agent branch has nothing left to check.skill.toolsbranch, only after D0, at advisory severity (S2) and resolving against a published tool-name registry. Feasible: the platform already publishes its tool names to the metadata service at boot (plugin.ts:538), so the D2 choice is between snapshotting that into aPLATFORM_PROVIDED_TOOL_NAMESset (thePLATFORM_PROVIDED_OBJECT_NAMES/ feat(lint): reference-integrity validation for object and action names (#3583) #3657 precedent) or staying advisory. Without one of those, the 6/16 false-positive rate above stands.[EXPERIMENTAL — not enforced]toagent.knowledge.indexes/sources. Adding aknowledgeSourcesstack slot would create a second namespace with no reader, i.e. the §2 problem again.Net: R7 as scoped is ~M of work that would find 10 real problems and 6 false ones, over two namespaces whose authoring status the platform has not settled. Settling D0 and finishing the §1 withdrawal is smaller, removes the false positives by construction, and is a prerequisite either way.
Generated by Claude Code
chore: changeset + regenerate app reference for defaultAgent doc (#3820)
os-zhuang commentedon Jul 28, 2026
Disposition update (per maintainer direction): the knowledge half (D1) is deferred entirely — not a current priority, no marker work either. The rest of the assessment above is now in flight as three PRs:
validate-ai-surface-affinityinREFERENCE_INTEGRITY_RULES(the statically provable, runtime-fatal AI check; zero FP by construction), the ADR-0063 doc-truth fixes (stack.agentsmarked platform-internal,app.defaultAgentre-documented as a surface-binding knob,SkillSchemadocuments that per-skillpermissionsdoesn't exist), and ADR-0109 (Proposed) — the D0 decision this issue is blocked on: third-party tools are bindings to actions/flows; platform tool names become a conformance-tested registry. CI green.loadAgent()now carries the same platform-ownership gate aslistAgents(), so a withdrawn tenant agent 404s on direct chat andapp.defaultAgentfalls back toask.permissionskeys — the field does not exist in SkillSchema hotcrm#511 — deletes the six deadskill.permissionskeys (ADR-0049 shape).R7's remaining buildable scope after this: the
skill.toolsbranch only, blocked on ADR-0109 acceptance (which replaces D0/D2 here), warning-first per the ADR-0078 ratchet.Generated by Claude Code
feat(lint,spec): AI surface-affinity rule, ADR-0063 doc completion, A…
feat(spec,lint): ADR-0109 revised + Phase 1 — skills-only default pat…
feat(spec,lint)!: accept ADR-0109, remove agent.tools[], lint agent a…
fix(spec): tombstone agent.tools instead of deleting it — unbreak main (
os-zhuang commentedon Jul 28, 2026
Batch complete — 10 PRs merged across three repos
objectstack
validate-ai-surface-affinityrule; ADR-0063 doc completion (stack.agentsmarked platform-internal,app.defaultAgentre-documented as a surface-binding knob); ADR-0109 draftedPLATFORM_PROVIDED_TOOL_NAMESregistry,validate-ai-tool-references(advisory),composeStacksstops droppingtoolsagent.tools[]removed;validate-ai-agent-authoringrule; exposure-awareaction_*resolution; docs teach the Action → skill pathcloud
loadAgent/resolveDefaultAgentplatform filteringPLATFORM_PROVIDED_TOOL_NAMESconformance suitesagent.tools11949fchotcrm
skill.permissionskey removedDisposition
errorseverity (statically provable, runtime-fatal),skill.toolsat advisory per the ADR-0078 ratchet. The 37.5% false-positive rate that originally blocked this branch is gone by construction: on the HotCRM corpus the rule yields exactly the 10 fictional references and 0 false positives.tools[]names a platform-registered tool or a materialisedaction_<name>. Tool records are demoted to an optional AI-presentation refinement layer (Phase 2, unscheduled).One correction worth recording
#3894 removed
agent.toolsandAIToolSchemaoutright. Its 16 checks were green, but that was a turbo cache restore onBuild Core— a cold build ofmainfailed, because the authorable-surface ratchet (ADR-0104 / #3733) rejects an authorable key disappearing without a tombstone. It was right to: these schemas are not.strict(), so Zod silently strips an unknown key, and an author who kept writingtools:would have gotten a clean parse and an agent reaching none of the tools they listed — the same silent-capability-loss shape this issue exists to eliminate, reintroduced one layer down by the PR meant to eliminate it.#3904 fixes it properly:
agent.toolsis nowretiredKey()(authoring it throws with the fix in the message), plus a D2 conversionagent-tools-to-skillsand its D3 chain step so the removal reachesspec-changes.jsonandos migrate meta. Unlike the protocol-17 renames beside it, this one has no lossless target — each entry must become a reference inside a skill, a human decision — so the conversion drops the dead key and emits a notice per agent rather than guessing a destination.mainbuilds cold again.Two things that follow from it and are worth a maintainer's attention independently:
tools"remains a silent no-op rather than a parse error." That is now false and fix(spec): tombstone agent.tools instead of deleting it — unbreak main #3904's changeset supersedes it.Open findings — maintainer decisions, not cleanups
1.
ASK_AGENT.skillsis a hardcoded allow-list, so third-party skills cannot attach toask. ADR-0063 §2 names skills the only third-party extension primitive, but there is no path by which a third-party skill reaches theaskagent:ask-agent.ts:82—skills: ['schema_reader', 'data_explorer', 'actions_executor', 'builder_handoff']skill-registry.ts:128-138—listActiveSkills(context, restrictTo)intersects the registry with that array (if (allowList && !allowList.has(skill.name)) return false), andagent-runtime.ts:467passesagent.skillsasrestrictToask-agent.ts:105-108—protection: { lock: 'full' }, so tenants cannot edit the persona to add a nameA third-party
surface: 'ask'skill registers successfully, passes validation, and is silently never activated — it does not even hit the ADR-0064 §3 affinity error, since that check only runs on skills that already cleared the allow-list. The comment directly above the lock states the promise this blocks: "Tenants extend the platform with skills + tools, never by editing this persona." Three possible fixes (drop the intersection whenskills[]is absent; a skill-sideattachTo; a personaextraSkillsoverride) all move the ADR-0063/0064 boundary, so this needs a call before implementation. Filing separately againstcloud.2. Nothing in the open distribution consumes skills at all — filed as #3905. The open framework is deliberately MCP-only (cloud ADR-0025), which is fine; the gap is that
skill/tool/mcpmetadata still ships in the open spec, is authorable, is actively linted by the rules added in this batch, and reaches no runtime there.Both are pre-existing and neither blocks this batch.
Generated by Claude Code
os-zhuang commentedon Aug 5, 2026
关闭核验(devx 车道 PM,
session_01GX3sL71LFq8m2usg6VqTSE,2026-08-05):本单交付物已全部在 main 上 ——packages/lint/src/validate-ai-surface-affinity*与validate-ai-tool-references*(含测试)存在且接入reference-integrity-suite.ts(本 PM 对 origin/main 实测命中);07-28 批次评论记录 10 个 PR 合并、D0/D2 由 ADR-0109(Accepted)回答、R7 的skill.tools分支按 ADR-0078 棘轮以 advisory 落地。D1(knowledge 半边)维护者已裁定整体搁置(2026-07-28 评论在案),不留在本单——届时重启是新立项。按发现分诊纪律关闭(completed)。维护者可否决重开。
Generated by Claude Code
test(spec): security, ai, identity, integration, migrations, marketpl…