Skip to content

docs(spec): the AI slot answers 501, not 404, when no AI service is mounted - #18597

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-17847-ai-slot-501-not-404
Sep 17, 2026
Merged

os-bill merged 1 commit into
mainfrom
claude/issue-17847-ai-slot-501-not-404

Conversation

@os-bill

@os-bill os-bill commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #17847

Clause-②: no

packages/spec/src/api/protocol.zod.ts's AI Operations note said this repo's dispatcher "404s AI service is not configured" when nothing serves the AI slot. It has answered 501 since the shared capabilityUnavailable exit landed, and the quoted body is no longer a local string either. This PR corrects that prose and nothing else — no runtime file is in the diff, and no status code, schema key or export moves.

Attribution for this run, in prose so it survives any body rewrite: produced by Claude Code, session session_01JbZnqu8bt6YqfJsr9vaFb3, https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3

The premise, re-measured — located by TEXT, not by line number

The card warns its line numbers rotted three times in one day, so the sentence was re-located by its text against origin/main e0d05538c0 (unmoved for the life of this branch):

$ grep -c '404s' packages/spec/src/api/protocol.zod.ts
1
$ grep -n '404s' packages/spec/src/api/protocol.zod.ts
2791:// `buildAIRoutes()` mounted, or 404s "AI service is not configured". So these

Counter-side, read the same round: packages/runtime/src/domains/ai.ts reaches capabilityUnavailable(deps, 'ai'), and packages/runtime/src/domains/unavailable.ts returns deps.error(serviceUnavailableMessage(slot), 501) under a docblock that states 501 Not Implemented in words. The defect stands, and the premise is live.

One extra reading the card did not carry: the literal string AI service is not configured survives nowhere in the tree except that stale comment (grep over packages/, content/, examples/, scripts/ — one hit, the comment itself). So the sentence was stale in both halves: the status AND the body it quoted.

Every arm was measured, not copied

The card hands over its behaviour table explicitly unverified, and an unqualified "/ai/* answers 501" would manufacture a second inaccurate sentence. Each arm is pinned by packages/runtime/src/domains/ai-anonymous-deny-ordering.test.ts, run on this tree:

pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 --reporter=verbose \
  src/domains/ai-anonymous-deny-ordering.test.ts
 Test Files  1 passed (1)
      Tests  13 passed (13)
arm the case that pins it answer
anonymous GET /ai/models → 401, not the 501 remedy sentence · GET /ai/agents → 401, not the 200 empty-list courtesy 401 first (ANONYMOUS_DENY_STATUS = 401, pinned in core/src/security/anonymous-deny.test.ts)
GET /ai/agents serves /ai/agents as the declared envelope with the payload relocated under data 200, { success: true, data: { agents: [] } }
every other /ai/* 501s /ai/models, /conversations, /usage and /chat with the remedy sentence 501
the body keeps the 501 body string-identical to /discovery's services.ai message shared serviceUnavailableMessage

⚠️ One condition deliberately not claimed in the new prose: a serveable service whose route table has not been published yet answers 503 AI service routes not yet initialized. That is a different premise from "no AI service is mounted", the sentence's own scope, and the three sibling sites do not state it either. Naming it here would widen the sentence back into the tour the card forbids.

The replacement

The same three-arm text the other three live sites carry after #16211 / PR #17844 (packages/client/src/index.ts, packages/runtime/src/route-ledger.ts, packages/runtime/src/domains/ai.ts), condensed to the block-comment voice this file already uses. 12 lines replace 2.

Changeset: owed, and measured rather than assumed

skip-changeset is not owed. packages/spec's files[] carries src/**/*.zod.ts, and npm pack --dry-run --json lists src/api/protocol.zod.ts among the tarball's 2016 entries (positive control: README.md present; negative control: zero *.test.ts). The sentence an upgrading author reads is a published byte, so it takes a patch. Not major, and Clause-②: no — check:api-surface and check:authorable-surface both green prove no export and no authorable key moved.

Gates

Derived off the merge base by the repo's own deriver, never by a hand-written diff:

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands   # 77 commands
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RANFILE
✓ dispatch-gates --ran: 77 derived famil(ies) accounted for — 75 run, 2 NOT-MEASURED
  (2 DERIVED from a recorded exit 3)

Every exit code was landed to a file before being read — never through a pipe.

  • 74 of 75 exit 0. Plus the 6 artifact-roster families the deriver flags because their roster sits under one of my paths (check-changeset-fixed, check:meta-url-spelling, check:spec-changes, check:authz-resolver, check:error-code-casing, check:filter-alias-parity) — read rather than assumed passed, all exit 0.
  • pnpm check:cross-package-test-inputs exits 1 — the known signature (packages/cli descends packages/spec/dist/), red on a tree where packages/spec has been built, green where it has not. Already filed as [finding] check:cross-package-test-inputs passes in CI and fails on a built tree — its verdict is a function of gitignored build state #18353 / [finding] check:cross-package-test-inputs answers 1 or 0 depending on whether packages/spec has been BUILT — the author who follows AGENTS.md is the only one who sees the red, and CI never does #18440; ⛔ not filed again and not caused by this diff.
  • 2 NOT-MEASURED, exit 3 = PREREQUISITE NOT MET: check:dual-build-cjs-loads and check:lean-entry-closure both read built output across the whole workspace and refuse on an unbuilt package. Neither a pass nor a finding; they belong to CI's whole-tree build.
  • pnpm --filter @objectstack/spec check:generated — all 15 generated artifacts up to date, check:docs included. The block is a bare // comment, not a .describe(), so it feeds no content/docs/references/** page: grep buildAIRoutes content/docs/references/ is empty while the positive control (a .describe() string from this same file) does land in content/docs/references/api/protocol.mdx. ⇒ nothing to regenerate, and nothing hand-edited under a generated tree.
  • Three spec gates first answered exit 1 as a stale-dist refusal (check:api-surface, check:browser-reachable-entries, check:dual-source-exports) — my src edit moved the build-input hash. All three exit 0 after pnpm --filter @objectstack/spec build, which is the documented caveat working, not a finding.

Tests

pnpm --filter @objectstack/spec test        Test Files  483 passed (483) · Tests  13781 passed (13781)
pnpm --filter @objectstack/spec typecheck   exit 0 (incl. check:test-typecheck, 144 pinned signatures held)

Dependency-closure build (step ①) is a no-op by construction: packages/spec declares no workspace dependencies, so packages/spec^... is empty. The package itself was built, because the dist-reading gates above require it.

Lint, as a declared narrowing rather than a repo sweep. pnpm exec eslint --no-inline-config --format json packages/spec/src/api/protocol.zod.ts → 1 file, 0 errors, 0 warnings, file count read from eslint's own JSON output. The checked population comes from eslint.config.mjs itself: every block matches **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} or a narrower glob, so the .changeset/*.md half of this diff is outside the linted set entirely and one file is the whole of it. Invariance for untouched files: this repo runs one eslint.config.mjs and it never enables type-aware linting — zero occurrences of projectService, and the only mention of parserOptions.project in the file is the comment stating it is absent — so no verdict on a file this diff did not touch can move because of this diff. The repo-wide pnpm lint stays CI's run.

No ablation and no reverse-verification run is reported, and that is a statement rather than an omission: this diff adds no guard that could be ablated and changes no type that could be made to fail. The claim the prose makes is carried by an existing pin test, which is why it was run rather than read.

Acceptance notes

  • content/docs/api/plugin-endpoints.mdx (hand-written) discusses the AI routes and does not repeat the false 404 claim — it simply says nothing about what an unmounted slot answers. An absence, not a wrong statement, so it is not one of the three fileable classes. noted, not filed; successor: none known — no queued PR touches that page's AI section.
  • The 503 AI service routes not yet initialized exit noted above is correct as it stands and is deliberately left out of the corrected sentence. Recorded as a scope boundary, not a finding.

Generated by Claude Code

…ounted

`packages/spec/src/api/protocol.zod.ts`'s AI Operations note said the
dispatcher "404s \"AI service is not configured\"" when nothing serves the
slot. It answers 501 via the shared `capabilityUnavailable` exit, and the
quoted body is no longer a local string — it comes from the shared
`serviceUnavailableMessage`, so the 501 body and `discovery.services.ai`
cannot drift.

The replacement carries the same three arms the other three live sites
gained in PR #17844: anonymous -> 401 first, `GET /ai/agents` -> 200 with
an empty list as a console courtesy, every other `/ai/*` route -> 501. All
three measured against `domains/ai-anonymous-deny-ordering.test.ts`
(13/13 passing), not copied from the card.

Prose only: no schema key moves, no accept set changes, no runtime edit.

Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/api/protocol.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/api/protocol.zod.ts) — pages documenting those are invisible to this run
  • 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.

Coarse fallback — 136 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 e0d05538c0d0275728d25acfafbab259e23d0710 → packageMentionDocs.

@os-bill
os-bill marked this pull request as ready for review September 17, 2026 07:51
@os-bill
os-bill enabled auto-merge September 17, 2026 07:52
@os-bill
os-bill added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit d93400f Sep 17, 2026
37 checks passed
@os-bill
os-bill deleted the claude/issue-17847-ai-slot-501-not-404 branch September 17, 2026 08:24
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…inding nothing ever resolved (objectstack-ai#18619)

Fixes objectstack-ai#16885

Clause-②: no

Retires `ListViewSchema.navigation.view` under ADR-0049
enforce-or-remove, executing the
maintainer ruling of 2026-09-13 (director decision batch objectstack-ai#126 item 4,
comment `5651023571`,
verbatim 「同意」, option **B**), with its item 5 superseded by comment
`5657440531`: the
changeset level is **`minor`**, not `major`.

## What the key did

`navigation.view` was an unconstrained string whose describe promised
*"the form view to use
for details"*. No layer from spec to console ever resolved a view by
that name. Its one read in
the shipped console passed the value into the **second argument of
`onNavigate`** — the slot
that otherwise carries the navigation-MODE token — so an authored name
did not select a view,
it **substituted for the mode**. A consumer in the same bundle reads
that argument against a
closed two-value vocabulary (`edit` / `view`), so any other authored
value matched neither
branch. Declared, consumed, and wrong.

## Acceptance criteria — executable, with both controls

**PROBE** — `navigation: { view: 'summary_view' }` is now refused, at
the key the author wrote,
with the prescription naming page assignment as the route:

```
PROBE  navigation:{view:"summary_view"} -> success = false
       path = navigation.view
       message = `view.list.navigation.view` was removed in @objectstack/spec 17.5.0
                 (ADR-0049 enforce-or-remove) — ... Delete the key; to choose what opens for a
                 record, assign a `record` page to the object and let `isDefault` pick the one
                 that opens — page assignment is the machinery that resolves a detail layout ...
LIT    navigation:{mode:"page"}         -> success = true
LIT    all five surviving keys          -> success = true
```

**LIT CONTROL** — the live siblings still parse. `{ mode: 'page' }` is
accepted, and so are all
five survivors together (`mode`, `preventNavigation`, `openNewTab`,
`size`, `width`). A tombstone
that broke its siblings would satisfy the refusal assertion while being
a larger bug, and
`navigation` is one closed shape, so that blast radius is the whole
block. Pinned at all three
doors — `ListViewSchema`, `ObjectListViewSchema`, and the flattened `PUT
/api/v1/meta/view`
overlay.

**DARK CONTROL** — the reading used is the regenerated
`packages/spec/authorable-surface/ui.json`
`ui/NavigationConfig:*` row set. It returned: **6 rows before, 6 rows
after**; exactly one line
changed, `ui/NavigationConfig:view` -> `ui/NavigationConfig:view
[RETIRED]`; the retired key
never appears plain, and **no other `ui/NavigationConfig:*` row
disappeared**.

```diff
     "ui/NavigationConfig:openNewTab",
     "ui/NavigationConfig:preventNavigation",
     "ui/NavigationConfig:size",
-    "ui/NavigationConfig:view",
+    "ui/NavigationConfig:view [RETIRED]",
     "ui/NavigationConfig:width",
```

## Gates observed RED before GREEN

A gate never observed failing for this change is not known to be a gate
for it.

| gate | red | green |
| --- | --- | --- |
| `check:generated` (`check:docs` leg) | `✗ 1 of 15 artifact(s) stale:
content/docs/references/**` | `✓ All 15 generated artifacts are up to
date.` after `--fix` regenerated exactly that one |
| `check:generated` (`check:api-surface` leg) | `✗ ... dist/**/*.d.ts
describe DIFFERENT sources than the ones on disk` (stale-dist trap,
after the test file moved) | `✓` after a real rebuild |
| `check-adr-0087-registration` | exit **1** — `declares a breaking
change (BREAKING) but no adr-0087: disposition marker` — proven by
committing the changeset with the marker line dropped | exit **0** — `1
declared-breaking changeset(s), each carrying an ADR-0087 disposition
... registered list-view-navigation-view-retired` |
| the three refusal pins | **3 failed / 8 passed** under an ablation
that restored the pre-retirement live string | **11 passed** on the real
tree |

The ablation mutated `packages/spec/src/ui/view.zod.ts` on disk
(tombstone occurrences 1 -> 0,
ablation marker 0 -> 1, blob hash moved), ran the pins, then restored
under a `trap` — restored
blob hash byte-identical to `HEAD` and `git diff HEAD` empty. Note the
direction: only the three
refusal pins went red; the eight sibling-acceptance assertions stayed
green, which is the correct
shape for reverting a tombstone.

## Gate results

`pnpm --filter @objectstack/spec check:generated` — **all 15 green**,
including the four named on
the card: `check:authorable-surface`, `check:api-surface`, `check:docs`,
`check:liveness`. Working
tree clean afterwards (no regeneration drift).

`pnpm --filter @objectstack/spec typecheck` — green. `pnpm --filter
@objectstack/spec test` —
**484 files, 13830 tests, all passed**. `pnpm lint` (repo-wide, `eslint
. --no-inline-config`) —
green, so no narrowing was taken and none is declared.

All 14 source audits `check:generated` deliberately does not run —
green.
`check:skill-examples` first reported PREREQUISITE NOT MET
(`packages/client-react/dist` unbuilt
in a fresh worktree); after building that closure it reads `✅ 258 prose
examples type-check
across 3 surface(s)`.

`node scripts/check-changeset-no-major.mjs` — green, no `major` bump.

**`check-widening-tells` did not recur.** `node
scripts/pm/check-widening-tells.mjs --declaration no
--diff <this diff>` exits **0**: `10 changed file(s) — 4 judged against
a declared surface (no
widening tell), 6 NOT MEASURED`. The T1/T2 firing on `retiredKey()`
lines recorded on objectstack-ai#17955 and
objectstack-ai#17300 did **not** reproduce here, so there is nothing to report as a
regression and the
declaration is unchanged, as ruled.

## The ADR-0087 disposition is SEMANTIC, deliberately

The ruling asked for a semantic migration entry, and the shape earns it:
a mechanical D2 strip
would delete the key without recording **which list view** lost it, and
an author who wrote it
wanted a named detail layout — a want page assignment serves and a
stripped key does not record.
So the entry is a D3 `SemanticMigration`,
`list-view-navigation-view-retired`, and the tombstone
prescription therefore carries **no** `os migrate meta` sentence: that
sentence is owed only where
a conversion covers the surface (`shared/retired-key.ts` module
docblock; the class pin
`retired-key-migrate-sentence.test.ts` deliberately judges nothing when
the marker is absent).
The precedent for registering a retired key with no D2 conversion is
`data/AggregationNode:distinct` in this same table.

Registered as `ui/NavigationConfig:view` in `RETIRED_KEYS_BY_MAJOR[18]`,
which also starts its
aging clock. Note for the reviewer: **no major-18 semantic entry reaches
`spec-changes.json` or
`docs/protocol-upgrade-guide.md` yet** — measured, not assumed: four
sampled major-18 ids
(`change-management-family-retired`, `training-family-retired`,
`scim-provider-object-retired`,
`epoch-instant-keys-renamed`) return 0 in both files, while major-17 ids
return non-zero and the
guide stops at "Protocol 17". This entry behaving the same way is the
steady state for the open
window, not a gap.

## Liveness ledger — nothing to update, and why

`view/list/navigation` is one `live` row with **no classified
children**, because `check-liveness`
drills one level. That is objectstack-ai#17424's subject; it is cited here and **not
fixed**, exactly as ruling
item 4 directs. Since the walk never reaches `navigation`'s children, no
ledger row exists for
`view` to mark `dead`, and none is owed — `check:liveness` is green with
the tombstone in place,
with no UNCLASSIFIED row. `packages/spec/liveness/**` is untouched (held
by objectstack-ai#18582).

## The objectui contract twin is in the SIBLING repo — reported, not
touched

Ruling item 3 asked where the contract twins live. **They are in
`objectui`, not here.** The
reading:

-
`objectui/packages/types/src/__tests__/view-navigation-config-spec-parity.test.ts`
at the pinned
`.objectui-sha` (`53ded82bf7a494f54e344e19099dbf00854b8694`) is the
twin. It asserts
`Equal<ViewNavigationConfig, NavigationConfig>` — `ViewNavigationConfig`
**is** this spec type,
  re-exported by reference since objectui#4588.
- That file **authors the retired key** as a legal value in four places,
e.g.
`const navigation: ViewNavigationConfig = { view: 'summary_view' };`.
Once objectui resolves a
spec carrying this tombstone, those lines become `tsc` errors under that
package's `type-check`.
- In **this** repo there is no twin pin covering the key: repo-wide,
`ViewNavigationConfig` occurs
only in `packages/console/CHANGELOG.md` (a release-owned file), and the
tracked
`sdui.manifest.json` declares `navigation` as a flat `object` input with
no drill into `view`,
so `check:react-declaration-parity` is structurally blind to it. Nothing
here to update.

**The Console Pin Gate is not affected and does not run on this PR.**
Measured: that job is gated
on the `console` paths filter (`.objectui-sha` plus four scripts), none
of which this diff touches;
and it runs `scripts/build-console.sh` — a **build**, which does not
type-check the sibling's
`tsconfig.test.json`. objectui's src at the pinned SHA contains **no
value write** of
`navigation.view` outside that test file (only docblocks), and reads of
`.view` still compile
against `never | undefined`. So this PR does not turn `main` red.

What is still owed, in the other repository: the twin gains the
tombstone pin before objectui
picks up a spec carrying this retirement. Its dependency is
`@objectstack/spec: ^17.0.0`, so a
published 17.5.0 is in range.

## File surface

Every path touched, including the pre-declared OPEN set.

| path | why |
| --- | --- |
| `packages/spec/src/ui/view.zod.ts` | the `retiredKey()` tombstone +
its prescription constant |
|
`packages/spec/src/migrations/entries/retired-keys/18.ui__NavigationConfig__view.ts`
| new — `RETIRED_KEYS_BY_MAJOR[18]` registration (build-schemas gate
(b)) |
|
`packages/spec/src/migrations/entries/semantic/18.list-view-navigation-view-retired.ts`
| new — the ADR-0087 D3 semantic entry |
| `packages/spec/src/ui/view.test.ts` | the pins: refusal at three
doors, plus the lit controls |
| `packages/spec/src/migrations/registry.ts` | OPEN set — generated by
`gen:migration-registry`, never hand-edited |
| `.changeset/16885-retire-navigation-view.md` | OPEN set — `minor`,
`**BREAKING**` banner, FROM -> TO, ADR-0087 disposition |
| `packages/spec/authorable-surface/ui.json` | OPEN set — regenerated by
`build` |
| `content/docs/references/ui/view.mdx` | OPEN set — regenerated by
`gen:docs` |
| `content/docs/references/api/protocol.mdx` | OPEN set — regenerated by
`gen:docs` (the inline navigation summary drops `view?: string`) |
| `content/docs/references/data/object.mdx` | OPEN set — same, one row |

Held by other in-flight work and **not touched**:
`packages/spec/scripts/build-schemas.ts` (objectstack-ai#17969),
`packages/spec/liveness/**` and
`packages/spec/scripts/liveness/check-liveness.mts` (objectstack-ai#18582),
`packages/spec/src/api/protocol.zod.ts` (objectstack-ai#18597). Ruling item 2 was
already discharged — PR objectstack-ai#17796
reads `closed`, not merged — so the retirement is written fresh.

## Acceptance notes

Noted, not filed: nothing in the touched files.

One finding **to file separately**, unrelated to this diff and not fixed
here:
`pnpm check:cross-package-test-inputs` reaches opposite verdicts on an
identical source tree
depending only on whether `packages/spec/dist/` happens to be built —
exit **1** with it present
(115 entries), exit **0** with the same directory moved aside, same
commit, restored afterwards.
The flagged radius is `packages/spec/dist/` descended from
`packages/cli/test/init-created-files-summary.e2e.test.ts`, which this
diff does not touch. No
`pnpm build` step precedes `pnpm check:cross-package-test-inputs` in
`lint.yml`'s `lint` job, so
CI always runs it against an unbuilt tree and it passes vacuously — the
declaration hole it exists
to catch is structurally invisible on every PR. That is the "a verifier
that silently degrades is
worse than no verifier" shape from AGENTS.md Route & surface ownership
rule 3.

## 维护者速读(草稿)

**改了什么** —— `ListViewSchema.navigation.view` 退役:键仍在 shape 里,但变成 ADR-0049
墓碑,
写它是 `tsc` 报错、传值进来是带处方的 parse 拒收。处方点名替代路线:给对象发布 `record` 页面,
用 `isDefault` 决定打开哪一个。同批落地 ADR-0087 的 D3 语义迁移条目、changeset(`minor` +
`**BREAKING**`)、以及三道门上的拒收 pin。

**为什么改** —— 这个键承诺"用哪个表单视图打开详情",而从 spec 到 console 没有任何一层按名字解析
视图。它唯一的读取把值塞进了 `onNavigate` 的**第二个参数** —— 那个槽位装的是导航**模式**令牌。
所以作者写的视图名不是在选视图,而是在**顶替模式**;而同一个 bundle 里有消费者拿这个参数去对
`edit`/`view` 两值词表,任何别的值两个分支都不匹配。仓库内零编写实例,唯一的外部作者已自行删除。
维护者 2026-09-13 裁决 B:零拉力即移除。

**风险与代价(含回滚)** —— 风险低。行为面没有回归可言:这个键从来没选中过任何东西,决定详情怎么
呈现的是 `mode` 和 `size`,两者一字未动,并且有 pin 钉着五个存活键仍然接受。代价是发布面:这是
**breaking**,升级时仍写着该键的元数据会在三道门(含 `PUT /api/v1/meta/view`)被拒。回滚就是
revert 本 PR —— 墓碑、注册条目、changeset 与生成产物都在同一个提交序列里,没有跨仓副作用。
一个待办留在**姊妹仓**:objectui 的契约孪生 pin 把 `{ view: 'summary_view' }` 写成合法值,需要在
objectui 取到带墓碑的 spec 之前补上墓碑 pin。本 PR 不触发 Console Pin Gate,不会让 `main` 变红。

**席位意见** ——

**你要做的** ——

---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

packages/spec states the AI slot 404s when no AI service is mounted — it has answered 501 since the shared capabilityUnavailable exit landed

2 participants