Skip to content

spec: define the confirmation contract behind action.ai.requiresConfirmation - #16531

Merged
huangyiirene merged 5 commits into
mainfrom
claude/issue-16293-action-confirmation-contract
Sep 7, 2026
Merged

spec: define the confirmation contract behind action.ai.requiresConfirmation#16531
huangyiirene merged 5 commits into
mainfrom
claude/issue-16293-action-confirmation-contract

Conversation

@huangyiirene

@huangyiirene huangyiirene commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16293

Clause-②: yes

Executes decision batch #54 (maintainer 「同意」 on recommendation A). This is the spec half only — it defines the contract and registers the refusal code. The runtime enforcement in invokeBusinessAction is #15942, which remains open and is not addressed here.

Contract review at CONTRACT_REVIEW_TIER (claude-fable-5-1): PASS — no blocking findings (comment 5567317321). Six advisories were held for a repair pass; all six have landed and are itemised under The repair pass below. The load-bearing decision was re-confirmed by that review and is untouched: the refusal gates on the DECLARED action.ai.requiresConfirmation === true, never on actionLooksDestructive, and the runtime is not in this diff.

The four deliverables

1. The request-side contractpackages/spec/src/contracts/ai-service.ts, directly under AIToolDefinition:

  • AI_ACTION_CONFIRMATION_MEMBER = 'confirm' — one exported constant fixing the member's spelling, so the door that refuses and the client that retries read the same symbol instead of each hand-spelling it.
  • AIActionConfirmation — the closed boolean member, declared once and mixed into each door's request shape rather than restated by it.

It rides at the top level of the action request, and the contract now NAMES the two request shapes it is destined for rather than gesturing at them: the MCP run_action tool input (actionName / objectName / recordId / params, packages/mcp/src/mcp-http-tools.ts) and the runtime action door's own request object — the input argument of invokeBusinessAction (objectName / recordId / params, packages/runtime/src/action-execution.ts), which the MCP bridge builds that object from.

Two placements were considered and rejected, and the reasons are written into the contract:

  • Not inside params. That bag is CLOSED against the author's own declared input vocabulary: enforceActionParams (ADR-0104 D2, strict by default since 17.0) rejects any key that is neither a declared param nor a built-in, so on an action that declares params a platform confirm riding there is REFUSED as an unknown action param — a 400 raised before the confirmation gate is ever reached. On an action declaring no params that check is a pass-through, so the same member would be silently accepted instead: one placement, two opposite behaviours, which on its own disqualifies it for a safety member.
  • Not a transport header. Invisible to the tool schema an agent reads, so the model cannot discover the retry it is being told to make; a header exists only at an HTTP edge while the action door itself is a plain function handed a request object; and a free-form header is an OPEN channel, which is the one thing a safety member must not be.

2. The refusalACTION_CONFIRMATION_REQUIRED, registered in ERROR_CODE_LEDGER under @objectstack/runtime (the package that owns both action doors, beside its ACTION_DISABLED sibling), answered 428. error.details is ActionConfirmationRequiredDetails: the action name, its object, and the exact member to set, so an agent builds the retry mechanically instead of re-parsing the prose it was handed.

3. The two misleading passagesai/tool.zod.ts's requiresConfirmation retirement guidance and the ADR-0049 semantic entry's replacement / acceptanceCriteria — rewritten so they no longer describe an approval queue, and (after the review) so they no longer assert enforcement that does not exist yet. See A4 below: the wording is contract-referential and says plainly that the flag stops nothing until the door lands.

4. Changeset minor on @objectstack/spec. Additive: three new exports, one new union member, zero removals — check:api-surface reports the surface unchanged apart from the additions.

Which predicate gates the refusal, and why

The DECLARED flag: action.ai.requiresConfirmation === true. Never actionLooksDestructive.

The MCP list_actions projection at packages/runtime/src/action-execution.ts:1014 reads

requiresConfirmation: actionLooksDestructive(deps, action),

and actionLooksDestructive (:979) returns the declared flag when the author set it and otherwise falls through to a destructiveness heuristic, per the #7828 Option A ruling. So the listing reports declared-flag-or-guess.

The ruling says "an action declaring ai.requiresConfirmation: true". Writing the refusal against actionLooksDestructive would make an action whose author declared nothing start refusing on a guess — well past what was ruled, and in the direction that breaks working callers.

The two are not the same predicate and the contract says so explicitly. They also answer different questions, so both are honoured without contradiction:

listing field refusal gate
asks should a client ASK the human? will the server REFUSE?
declared true true refuses without the member
declared false false never refuses
declared absent, heuristic hit true (heuristic) does not refuse

The member is accepted on every AI-facing action call and required only on the declared-gated ones. So a client that confirms whenever a listing says requiresConfirmation: true is always correct — over-confirming is free — while inferring "no door will ever refuse" from a listing's false is sound precisely because the listing's predicate is the wider of the two. list_actions is unchanged.

Why 428, and why a ledger code beside PRECONDITION_REQUIRED

The card rules 4xx: the request is valid but incomplete, and the caller can fix it. 428 is the platform's own reading of exactly that condition — packages/cli/src/utils/secret-reference-union.ts:189 already records it as "the standard catalog's 'request is missing a required precondition'". 409 was rejected: there is no state conflict, and the identical call with the member set succeeds against unchanged server state. 422 was rejected: the definition is fine.

The code is not a re-spelling of the standard member. PRECONDITION_REQUIRED says some precondition is missing, which leaves a caller unable to tell a confirmation gate from a missing conditional header and unable to build the retry; this code says which precondition, on which action — the same discrimination its *_DISABLED and FLOW_* neighbours make. Measured: standardSynonymOf('ACTION_CONFIRMATION_REQUIRED') returns undefined, with CONFLICT and FORBIDDEN lit as positive controls in the same run — now a standing pin rather than a one-off probe.

Registered ahead of its producer by design — the FLOW_INPUT_SCHEMA_INVALID split shape the ledger already sanctions — and the row cites the cards that make it a split rather than a residue (#16293 for this half, #15942 for the producer, decision batch #54 for the ruling that split them).

Deliberately not done here

The repair pass — the six advisories held from the review

All measurements below were taken in a worktree pinned to merge-base a5eccf9257, at final head 10abca6c28.

  • A1 — a false precedent citation, corrected. The row cited UNIQUE_SCOPE_CONFIRMATION_REQUIRED as "registered beside the same standard member for the same reason". Measured: that gate answers 409 (packages/cloud-connection/src/marketplace-install-local-plugin.ts:829 closes }, 409); its own ledger row says 409; HttpStatusErrorCodeMap maps 409 to RESOURCE_CONFLICT and 428 to PRECONDITION_REQUIRED). It is therefore a sibling in KIND only — the platform's other confirmation gate that registers its own code — and no precedent for choosing 428 over 409. 428 itself is unchanged.
  • A3 — the producerless row now cites its cards. [#16293] heads the row in the sibling convention, and the producerless paragraph names MCP run_action never enforces ai.requiresConfirmation — it is surfaced in list_actions only, while @objectstack/spec guidance tells authors it stops execution #15942 and decision batch Prepare v0.2.0 release for ObjectStack packages #54, with one sentence saying why the numbers are the point: a producerless row with no card behind it is the "registered but unemittable" retirement class, and this one is a split.
  • A4 — SECURITY: the prose is contract-referential until enforcement exists. The rewritten guidance and the migration entry asserted present-tense enforcement, and the new acceptanceCriteria told an author to "invoke it WITHOUT the confirmation member and observe the call refused" — an instruction that, before MCP run_action never enforces ai.requiresConfirmation — it is surfaced in list_actions only, while @objectstack/spec guidance tells authors it stops execution #15942, executes the destructive action. All three sites now describe what the contract DECLARES, say plainly that the door which performs the refusal has not landed, and explicitly warn against that verification: "until that door ships, such a call is not refused, it RUNS". The queue language is NOT restored — it stays deleted, exactly as the ruling requires. Nothing here relies on release ordering.
    • The third site is the changeset body, corrected in the same pass. It is the release-time carrier of the identical claim, so leaving it present-tense would have published the louder version of it at the next cut — the failure mode A4 names. Its grade is unchanged (minor, additive).
    • The tracker ids sit in a code comment beside the guidance table, not in the prescription string: check:doc-authoring reds on an internal issue id inside a customer-facing prescription (a hoisted guidance const is one of its recognised positions; ADR ids stay, #NNNN does not). The migration entry's replacement / acceptanceCriteria are not swept — measured against dozens of sibling semantic entries that carry #NNNN in those same two fields today, on a green tree — so both name MCP run_action never enforces ai.requiresConfirmation — it is surfaced in list_actions only, while @objectstack/spec guidance tells authors it stops execution #15942 directly.
  • A5 — a factual error corrected, and the stronger reason added. "Flow call action nodes" is gone: FlowNodeAction (packages/spec/src/automation/flow.zod.ts:27) enumerates script / subflow / connector_action and no action-call node (control: subflow is present in the same enum). Added, as the decisive reason the contract was missing: params is strict by default, so an undeclared confirm there is REJECTED rather than merely colliding — with the asymmetry spelled out, since enforceActionParams is a pass-through for an action that declares no params.
  • A6 — the missing pin. Measured before: git grep -l over *.test.ts / *.spec.ts returned 0 files for each of AI_ACTION_CONFIRMATION_MEMBER, AIActionConfirmation, ActionConfirmationRequiredDetails and ACTION_CONFIRMATION_REQUIRED, while the same glob lights 26 files for ERROR_CODE_LEDGER and the same terms light non-test files. packages/spec/src/contracts/action-confirmation-contract.pin.test.ts now carries the feat(spec): register FLOW_INPUT_SCHEMA_INVALID — the never-dispatched ADR-0112 code for the definition-level input-schema refusal #12611 shape — an accepts case through ErrorCode.parse, the ledger row under @objectstack/runtime (plus a control that no other owner holds it), standardSynonymOf(...) undefined against two lit controls — and the compile witness: confirmationMember typed to typeof AI_ACTION_CONFIRMATION_MEMBER, with a near-miss spelling and a string confirm each behind a @ts-expect-error. Those two directives are LIT rather than decorative: an unused directive is TS2578, this file carries no entry in test-typecheck-debt.json, and check:test-typecheck is green — which is only possible if both are consumed.
  • A7 — the second enumeration site, removed rather than pinned. The contract docblock no longer spells out the heuristic's legs; it references actionLooksDestructive by name and says the signals are named once, beside the authorable key in ui/action.zod.ts. Why removal over extending the pin: the enumeration is a RUNTIME implementation detail, and ActionAiSchema.requiresConfirmation docblock still names confirmText as a destructive signal — the #7828 ruling removed that leg #13865's pin exists to hold ONE authoring surface against the classifier. Extending it to a second site would freeze the same list in two places and make both rot-prone; deleting the restatement leaves nothing to rot and keeps the pin's subject singular. The changeset keeps its one mention, deliberately: it is frozen into CHANGELOG.md at release as a record of the decision, not a live authoring surface.

Accepted as-is per the review's rulings: A8 (the acceptanceCriteria rewrite is in-spirit and kept, with the A4 tense fix), A9 (minor is correct — not re-graded), A10 (actions-as-tools.mdx is #15942's).

Verification — all readings on 10abca6c28, merge-base a5eccf9257

  • Derived gates: 102 derived, 102 run, 0 UNRUNnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ... reconciles clean at this head. 100 green; 2 NOT MEASURED, both exit 3 (PREREQUISITE NOT MET, which the scripts themselves say is neither a pass nor a finding) and both needing a whole-tree build this container cannot fit in one foreground window: check:dual-build-cjs-loads (no new module and no new import edge hides behind it) and check:type-check-debt (its one moved package, @objectstack/spec, is separately green below, source + scripts + test layers). Both are declared to CI. Four further gates first returned exit 3 or a build-first exit 1 and were re-run to a real green after building the @objectstack/lint, @objectstack/client and @objectstack/client-react closures.
  • pnpm --filter @objectstack/spec check:generatedall 15 generated artifacts up to date, after gen:migration-registry and then exactly the four --fix proved stale (gen:spec-changes, gen:upgrade-guide, gen:skill-refs, gen:docs) — never the whole set. Every regenerated diff was read: the registry, spec-changes.json and the upgrade guide carry the corrected prose and nothing else.
    • One regeneration was a REAL finding, not noise: the A4 rationale was first written as a // block above TOOL_RETIRED_KEY_GUIDANCE, which detached that const's own JSDoc and made the doc generators re-read it as the module summary — rewriting content/docs/references/ai/tool.mdx and skills/objectstack-ai/references/_index.md. The comment now sits INSIDE the object literal beside the key it explains, and both files are untouched in this diff.
  • pnpm --filter @objectstack/spec test484 files / 13142 tests passed (from 483 / 13136: the new pin). pnpm --filter @objectstack/spec typecheck green across tsc --noEmit, check:scripts-typecheck and check:test-typecheck (54 files / 261 errors / 145 pinned signatures held, unchanged — the new file adds none).
  • Repo-wide lint, not narrowed: eslint . --no-inline-config over 6274 files — 0 errors, 0 warnings, run at this head (6273 before; the delta is the new test file).
  • check:nul-bytes green; plus an independent control-byte sweep of the 13 changed paths (grep -naP over the control ranges), no hits.
  • Gates re-run after the final changeset edit and green at this head: the three changeset gates, check:changeset-gate-self-tests, check:objectui-changeset, check:doc-authoring, check:keyed-text-bounds, check:nul-bytes, check:error-code-casing, check:comment-mask-adoption, check:comment-mask-corpus, check:closing-keyword-parity, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:dispatcher-error-vocabulary, check:error-code-provenance.
  • ⚠️ One anchoring caveat, recorded rather than hidden: dispatch-gates reports this tree as at least 24 commits behind origin/main, with five family-defining files (two workflows, scripts/nightly-tiers.*, check-skill-line-ratchet.mjs) changed across that range. The derivation is therefore against THIS branch's copies of those files. The change set itself is anchored to the literal merge-base a5eccf9257, never to a moving ref.

Review posture

needs:contract-review stays on this PR and on #16293. The PR stays draft — not flipped ready, not enqueued, no auto-merge — per the reviewing seat's instruction.

Authored by Claude Code in session session_01T6HeZvT9wdSJD1ZxJb5Eno (durable attribution, kept in prose).

…irmation`

Declares the request-side member, the refusal code and the refusal detail
shape, and rewrites the two passages that described a queue the open
framework path does not have.

- `AIActionConfirmation` / `AI_ACTION_CONFIRMATION_MEMBER` /
  `ActionConfirmationRequiredDetails` in `contracts/ai-service.ts`.
- `ACTION_CONFIRMATION_REQUIRED` in `ERROR_CODE_LEDGER` under
  `@objectstack/runtime`, answered 428, registered ahead of its producer.
- The refusal is gated on the DECLARED `ai.requiresConfirmation === true`,
  never on `actionLooksDestructive`'s heuristic fallback.

Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
Co-authored-by: Claude <noreply@anthropic.com>
api-surface / export-origins gain exactly the three new contract exports;
the ledger's reference page and ApiErrorSchema's code enum gain the one new
code; spec-changes.json and the upgrade guide pick up the rewritten
ADR-0049 entry prose. No removals in any of them.

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

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 10 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json, packages/spec/spec-changes.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via actionName (symbol, a field of interface ActionConfirmationRequiredDetails), requiresConfirmation (symbol, a field of const object TOOL_RETIRED_KEY_GUIDANCE))
  • content/docs/ai/tools.mdx (via requiresConfirmation (symbol, a field of const object TOOL_RETIRED_KEY_GUIDANCE))
  • content/docs/api/client-sdk.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/protocol/objectui/actions.mdx (via requiresConfirmation (symbol, a field of const object TOOL_RETIRED_KEY_GUIDANCE))
  • content/docs/protocol/objectui/record-alert.mdx (via actionName (symbol, a field of interface ActionConfirmationRequiredDetails))
  • content/docs/ui/actions.mdx (via actionName (symbol, a field of interface ActionConfirmationRequiredDetails))
  • content/docs/ui/apps.mdx (via actionName (symbol, a field of interface ActionConfirmationRequiredDetails))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/export-origins/contracts.json, packages/spec/spec-changes.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: objectName (symbol, 34 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 130 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 ce8caba91403c8f160cb7764c63b08371a13db99packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7dfa88cca71afdaf94f7243bb4b6bbe2da18e493 — the merge of head 10abca6c2825c3b91e113be6f56ceb3183568adc into base ce8caba91403c8f160cb7764c63b08371a13db99, 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 7dfa88cca71afdaf94f7243bb4b6bbe2da18e493 && git checkout 7dfa88cca71afdaf94f7243bb4b6bbe2da18e493
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce8caba91403c8f160cb7764c63b08371a13db99 10abca6c2825c3b91e113be6f56ceb3183568adc && git checkout -B drift-repro ce8caba91403c8f160cb7764c63b08371a13db99 && git merge --no-ff 10abca6c2825c3b91e113be6f56ceb3183568adc

node scripts/docs-audit/affected-docs.mjs --json ce8caba91403c8f160cb7764c63b08371a13db99

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ce8caba91403c8f160cb7764c63b08371a13db99 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review at CONTRACT_REVIEW_TIER: PASS — no blocking findings. Ten advisories, and I am holding the PR for a repair pass on six of them.

Reviewed at claude-fable-5-1 by an isolated reviewer, head 09248ca069, base a5eccf9257 (confirmed merge-base). ⚠️ Recorded because it is the right method and I want it copied: the reviewer worked in a detached worktree with no build, so no gate's baseline could move under it — git status --porcelain empty after every gate. That is the clean answer to the regenerate-your-own-baseline confound.

This seat runs claude-opus-5, below tier, and ⛔ did not self-review.

The load-bearing decision is CONFIRMED

  • Nothing binds the refusal to actionLooksDestructive or to the listing. actionLooksDestructive (:979-982) and the :1014 projection are untouched — the runtime is not in this diff.
  • The asymmetry holds. Listing true ⇒ either declared-true (refuses; sending the member fixes it) or heuristic-true (no refusal; member accepted and ignored) — a confirming client is correct both ways. Listing false ⇒ the refusal predicate is false in both of its cases. The one residual is by design and is what the ruling chose: a true listing cannot tell a client whether the server would have stopped it.
  • Measured at head: standardSynonymOf('ACTION_CONFIRMATION_REQUIRED') → undefined with CONFLICT/FORBIDDEN lit as controls; ErrorCode accepts the code and rejects a one-char typo; registered under exactly @objectstack/runtime; five @ts-expect-error legs consumed from a src probe, and ablating one directive yields a real TS2820 — the detector is lit. 4 test files / 77 tests pass.
  • check:dual-build-cjs-loads (the author's declared NOT MEASURED): nothing load-bearing hides behind it — no new module, no new import edge.

⚠️ Two advisories that are more than cosmetic

A4 — the prose creates an author-facing window the PR's own logic was written to avoid. The rewritten guidance and migration acceptanceCriteria assert present-tense enforcement the runtime does not perform until #15942. Worse, the new acceptance criteria instructs an author to "invoke it WITHOUT the confirmation member and observe the call refused"before #15942 lands, following that instruction executes the destructive action.

⇒ This is not the dev's error: the card mandated rewriting those passages. It is a real tension between the card's instruction and the card's purpose, and it lands on me. I am taking the reviewer's option (b): make the wording contract-referential until enforcement exists, rather than relying on a release-gating promise.

A2 — the hand-off to #15942 is a live trap. The MCP door strips confirm twice over today (SDK objectFromShape drops unknown keys; the handler forwards only { objectName, recordId, params } at mcp-http-tools.ts:762). ⇒ If #15942's dev enforces only in invokeBusinessAction, every declared-gated action becomes un-invokable via MCP with no way to confirm. Three files in two packages must move together. I am recording that on #15942 now so it cannot be lost.

The repair pass — six advisories, dispatching when a dev slot frees

⛔ The PR stays draft and unarmed until these land. It is priority:p1 + security; "PASS" is not a reason to ship prose that misleads an author about whether a destructive call will be stopped.

Accepted as-is


Generated by Claude Code

…/A7)

A1: the ledger row cited UNIQUE_SCOPE_CONFIRMATION_REQUIRED as registered
beside the same standard member; it answers 409 and so sits beside
RESOURCE_CONFLICT. Citation corrected; 428 unchanged.

A3: the producerless row now cites #16293, #15942 and decision batch #54.

A4: the retirement prescription and the migration entry asserted present-tense
enforcement the runtime does not perform yet, and told an author to invoke a
destructive action without the confirmation member "and observe the call
refused". Both are now contract-referential and warn that such a call RUNS
until the door lands.

A5: dropped the non-existent flow `call action` node from the placement
rationale and added the stronger reason -- `params` is strict by default
(enforceActionParams, ADR-0104 D2), so an undeclared `confirm` there is
rejected, not merely a collision. The two destination request shapes are now
named.

A6: added the missing pin -- ErrorCode admission, the ledger row, the
standard-synonym reading and a compile witness for confirmationMember.

A7: the contract no longer re-lists `mode: 'delete'` / `variant: 'danger'`; it
references actionLooksDestructive, leaving one enumeration site under the
#13865 pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
… A4 tense fix

The A4 rationale rides in a comment INSIDE the guidance literal rather than
above the const: placed above it, it detached the const's own JSDoc and the
doc generators re-read that block as the module summary, rewriting
content/docs/references/ai/tool.mdx and skills/objectstack-ai/references/_index.md.
Both are untouched again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
The changeset body is the release-time carrier of the same claim the guidance
and the migration entry were carrying, so leaving it present-tense would
publish exactly the sentence the repair pass removed. It now says the contract
is a declaration, that no door performs the refusal yet, and it takes the A5
correction on the `params` placement reason.

Grade unchanged: minor, additive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno

Copy link
Copy Markdown
Collaborator Author

Delta re-review at CONTRACT_REVIEW_TIER (claude-fable-5-1) — PASS, no blocking findings

Head 10abca6c28, merge-base a5eccf9257, delta 09248ca069..10abca6c28 (3 commits, 9 files). Method: two detached worktrees at head, --frozen-lockfile --ignore-scripts; one kept pristine for gate readings, one for ablations with a git diff --stat landing proof on every mutation and a revert to porcelain 0 after each.

All six held advisories land correctly. The review did not take the repairs on trust — each was re-measured:

  • A1 — the false precedent is properly corrected, not merely softened. UNIQUE_SCOPE_CONFIRMATION_REQUIRED was independently confirmed to answer 409 (marketplace-install-local-plugin.ts closes }, 409); its ledger row says 409; errors.zod.ts:178-179 maps 409→RESOURCE_CONFLICT, 428→PRECONDITION_REQUIRED). The row now claims sibling-in-kind only. The 428 justification is byte-unchanged — the correction removed a bad citation without weakening the decision it was wrongly supporting.
  • A3// [#16293] matches the file's own head form, measured across 36 sibling rows; "registered but unemittable" is the ledger header's own term, so split-vs-residue is drawn on the ledger's line rather than the author's.
  • A4 (SECURITY) — all four sites re-read in full, with the strings joined before grepping because they are 'a ' + 'b ' chains and a line-oriented search would have missed them. Each carries both required clauses: the refusal is declared but not performed, and do not try to verify it by invoking the operation — because before MCP run_action never enforces ai.requiresConfirmation — it is surfaced in list_actions only, while @objectstack/spec guidance tells authors it stops execution #15942 that call RUNS the destructive action. Queue language stays deleted. The tracker-id split was verified as a real constraint, not a story: planting #15942 inside the prescription string reds check:doc-authoring (exit 1, landing proof), green at head.
  • A5FlowNodeAction re-enumerated: no action-call node, with subflow lit as a control. The enforceActionParams asymmetry is confirmed at action-execution.ts:1186 and action-params.zod.ts:45-51.
  • A6 — the pin is lit for the right reason, and this is the one place the review upgraded its own method. The author's TS2578 argument establishes that the @ts-expect-error directives are consumed, but not why; the reviewer judged that insufficient, took the ablation that does answer it (delete the near-miss directive → TS2322: Type '"confirmed"' is not assignable to type '"confirm"' on that exact line), and it reads right. Three further ablations behave as predicted. check:test-typecheck at 54 / 261 / 145 — the author's numbers exactly.
  • A7 — de-enumeration confirmed as the right call: a single-token sweep finds the heuristic's legs at exactly one site, the ActionAiSchema.requiresConfirmation docblock still names confirmText as a destructive signal — the #7828 ruling removed that leg #13865-pinned one. A second pinned copy would freeze the list twice.

The generator-detachment claim is verified, and it was worth verifying: content/docs/references/ai/tool.mdx and skills/objectstack-ai/references/_index.md are blob-identical at merge-base and head, no commit touches either, and the JSDoc now sits directly above the const with the rationale inside the literal.

The api-surface leg that was left open is now closed. In a no-build tree it was a prerequisite failuredist holds no .d.ts — which is not a reading and was correctly refused as one. Taken separately with a 3m11s build from head source: porcelain empty afterwards (so the build's regeneration of authorable-surface/api.json produced no diff, which is the confound that makes this leg go falsely green when done carelessly), then check:api-surface"public API surface + factory signatures unchanged", exit 0.

Not measured, and declared as such: check:dual-build-cjs-loads and check:type-check-debt — the exit-3 PREREQUISITE NOT MET pair. CI's to answer, and CI has: both green in the run below.

Five advisories, none blocking, none held

Changeset line 10 and the "REAL gate" phrase keep indicative mood but are framed unambiguously by the surrounding sentences; the contract docblock's "a 400" is exact for the REST door while the MCP door returns a tool error; "named once" is true within packages/spec though the #13865 pin header names two prose carriers elsewhere; and the A6 method note above. None is worth another repair round.

CI — read in full this time

37 check runs, all 37 read (the listing pages at 30, and I am not making that mistake twice today): every one success or skipped, Lint & Repo Gates success. 13 files changed, none on the governed surface.

One thing checked because #16554 landed after this branch's base: packages/spec/api-surface/contracts.json is modified here, but ./contracts is not among the ten entry points #16554 rewrote, so there is no regen collision to resolve and the merge=os-regen routing does not come into play. (GitHub runs no custom merge driver, so a collision there would have surfaced as a plain conflict, not a silent resolution.)

Arming. needs:contract-review struck — the review it names has passed.


Generated by Claude Code

@huangyiirene
huangyiirene marked this pull request as ready for review September 7, 2026 09:59
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit fe0d9a4 Sep 7, 2026
42 checks passed
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 protocol:ai size/m tests tooling

Projects

None yet

2 participants