fix(spec): state the confirmation gate in the present tense — three shipped prescriptions were denying a door that exists - #19234
Conversation
Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…runs Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…tion-gate prescriptions Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 19bc03098b5427106ab5ff3b99b7521d21b1cfe1 && git checkout 19bc03098b5427106ab5ff3b99b7521d21b1cfe1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e233db9dbb9c43816b1f2c44a3381c96402334a3 fe911445a65c84611f52f24b96a2cfa0bcaa78df && git checkout -B drift-repro e233db9dbb9c43816b1f2c44a3381c96402334a3 && git merge --no-ff fe911445a65c84611f52f24b96a2cfa0bcaa78df
node scripts/docs-audit/affected-docs.mjs --json e233db9dbb9c43816b1f2c44a3381c96402334a3
|
Contract reviewServed-tier: Contract-tier review of record for PR #19234 (card #17487, ① Derived judgments1. The over-promise question — each claim in the three shipped strings, re-derived against the runtime (
⇒ No shipped claim exceeds what the runtime delivers, and the two bounds are the ones the door's docblock (l.1013-1049) declares, not wider. Nothing blocking under item 1. One understatement elsewhere is noted under ③. 2. Every false prescription is now true; no site that was not false was edited; the carrier count.
3.
4. The pin is a real pin — four legs run here, direction predicted before each run, every mutation and restore proven by blob.
Each leg ran through 5. The generated projections are regeneration output. With the spec built at this head, 6. The changeset. ② Semver level
③ Boundary flags
BLOCKING: none. noted (none of these is a defect of this PR; each is outside its declared surface or an understatement rather than a false claim):
Implemented-by: VERDICT: PASS Contract-tier reviewer · Generated by Claude Code |
Fixes #17487
Clause-②: no
The defect, and its direction
Three shipped, customer-facing prescriptions in
@objectstack/specstated in the present tense that the runtime confirmation door had not shipped. It has:actionConfirmationRefusalis called pre-dispatch byinvokeBusinessActionin@objectstack/runtime, and the MCPrun_actiontool grew theconfirmmember in the same change (the card behind it, #15942, is done —state_reason=completed; its changesetaction-confirmation-gate-enforcedis still pending, so the door is onmainand not yet released).So the published text denied a door that exists, and it failed in the dangerous direction: an author who reads it concludes the safety flag stops nothing, and either arranges a human in the loop some other way or stops setting the flag — losing the gate at the moment it starts working. That is the ADR-0049 false-compliance class with the sign flipped.
Re-derivation — all three sites read on today's
origin/mainTriage's unblock comment verified site 1 only and said the other two were unmeasured. All three were re-read at merge base
805811e0d.packages/spec/src/ai/tool.zod.ts—TOOL_RETIRED_KEY_GUIDANCE.requiresConfirmationpackages/spec/src/migrations/entries/semantic/17.tool-requires-confirmation-retired.ts—replacementacceptanceCriteriaCorrection to the card's count of the carriers. The card names the
spec-changesentry, the upgrade guide and theos migrate metaprojection as if they were separate sites. They are not: all three are projections of the one ADR-0087 D3 entry file above. The measurement is therefore three false prescriptions living in two source files, plus three generated artefacts that carry them (src/migrations/registry.ts,spec-changes.json,docs/protocol-upgrade-guide.md), all regenerated here bycheck:generated --fix.Sweep radius for "is that all of them": eleven denial phrasings grepped repo-wide (
not yet the behaviour,ships separately,not yet performed,stops nothing,simply RUNS,until it does,until that door,door lands,yours to arrange,nothing server-side,no pause), withrequiresConfirmationlighting 10 files underpackages/spec/srcas the positive control. Two adjacent texts were read and left alone as NOT A DEFECT:packages/spec/src/contracts/ai-service.tsalready states the gate in normative present tense, andcontent/docs/ai/tools.mdxsays the retired tool-level key "returns only together with its enforcement", which is still true — the tool key has not returned. Two further readings are recorded under Acceptance notes.What the prose says now, and what holds it there
Each prescription now states the refusal in the present tense with the door's bounds, because an unbounded "the platform refuses unconfirmed calls" is this same defect in the other direction. Read off the door's own docblock and its shipped changeset, never inferred:
ACTION_CONFIRMATION_REQUIRED, 428, naming the action and the memberconfirm: true;loadActionSubjectRecord, so no record is read and none written;ai.exposedopt-in — today the action door reached from MCPrun_action. REST/actionsis notai.exposed-gated and sits outside the gate, so an API-key agent on that route still needs its own human;ai.requiresConfirmation: truerefuses, and only the booleantrueconfirms; the widerlist_actionsheuristic advises and never refuses;confirm: trueis an unverifiable caller claim: the gate makes forgetting loud, it does not prove a human.packages/spec/src/ai/tool-confirmation-prescription-tense.pin.test.tsis the tie that was missing the first time — the prose was never bound to the function it describes, which is how it rotted. It reads the three shipped strings and the runtime door, and fails in both directions.No pin was moved.
ui/action-requires-confirmation-docblock.pin.test.tswas read: it anchors on theai.requiresConfirmationJSDoc inui/action.zod.tsand onactionLooksDestructive, neither of which this diff touches, so it covers none of the three sites and stays as it is.Clause-②: no — the accept set did not move
check:authorable-surfaceandcheck:api-surfaceare green with zero diff underpackages/spec/authorable-surface/andpackages/spec/api-surface/. The pin's last case feeds the same authored metadata in before and after:tool.requiresConfirmationstill refused, a minimal tool still accepted,action.ai.requiresConfirmationstill accepted for bothtrueandfalse. What moved is string content insidedistandspec-changes.json, which is why apatchchangeset is owed and present.Tests, and the reverse verification
pnpm --filter @objectstack/spec test— 499 files / 14614 tests passed.test:repo— 34 files / 580 tests passed.typecheck— clean. New pin: 8/8.Three ablation legs, each mutated on disk through
scripts/ablation-replace.mjs(anchor hit declared, blob hash proven to move), direction predicted before the run, restored and proven by blob hash againstHEADwithgit diff HEADempty:The refusal is DECLARED, not yet performedinto the D3 entry'sreplacementinvokeBusinessAction/actionsdoor name the gateLeg 3's first attempt was a no-op and is reported as such: the replacement text still contained the anchor, so
ablation-replacerefused (anchor drop 0, not the declared 1) and nothing ran. It was re-anchored and re-run; the reading above is the re-run.Gates
All 85 commands derived by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfor this diff were run locally and exit 0, exit codes captured before any pipe. Eight first returned a stale-distorPREREQUISITE NOT METresult (exit 1 / exit 3 — not measured, not findings); they were re-run green afterpnpm --filter @objectstack/spec buildand a fullturbo run buildclosure.pnpm lint(eslint . --no-inline-config, whole repo, no narrowing) exits 0 atHEAD. CI still owns its own farm: the five path-scheduled CI jobs, the 11 wide-population families and the artifact rosters are outside that 85 and are NOT MEASURED here.Acceptance notes
Two readings taken while re-deriving, both out of scope for this card and neither edited here:
packages/spec/docs/MCP_GUIDE.md(around the "Side Effects" section) tells an author to gate side effects with "ai.requiresConfirmationon the underlying action (+ the HITL approval queue)" and then warns, in the adjacent block, that "nothing server-side pauses on it". The warning is correctly scoped to the MCP capability descriptor in that page's examples and is true of it; but the approval-queue requirement now overstates what the action-level flag needs, and the two paragraphs read together in the card's own dangerous direction. Not in the declared file surface. Reported for filing with dedupe words:MCP_GUIDE,requiresConfirmation,HITL approval queue,nothing server-side pauses,confirmation gate.content/docs/ai/actions-as-tools.mdx— the "Human-in-the-loop approval" section still says that on the open MCP path "the approval step lives at the protocol boundary" (client-side prompting), and the numbered open-MCP action-gate list enumerates five gates without the confirmation gate that now sits between the param contract and the subject-record load. An omission against a contract that@objectstack/spec/contractsdeclares. Reported for filing with dedupe words:actions-as-tools,human-in-the-loop,protocol boundary,run_action,confirmation gate.Noted, not filed:
packages/spec/src/api/error-code-ledger.zod.tssays of theACTION_CONFIRMATION_REQUIREDrow that "the door will assert this exact string by value" — a forward tense about something that is now true. It misleads nobody about the gate and it is provenance prose about the row's split registration, not a prescription. Successor: the next change that touches that ledger row.Occupancy
Re-scanned at 2026-09-20T01:52Z over all 21 open PRs, with PR #17076 (639 files) fully paged so no path is under-read.
packages/spec/src/ai/tool.zod.ts, the D3 entry,spec-changes.json,docs/protocol-upgrade-guide.md,vitest.repo-tests.jsonandsrc/ai/tool.test.tsall read FREE. Firing controls in the same scan:packages/spec/src/ui/component.zod.tsHELD by #19219,packages/spec/src/ui/view.test.tsHELD by #19226; dark control (a nonexistent path) reads FREE. One reading to flag:packages/spec/src/migrations/registry.tsreads HELD by #19223, #19090 and #18319 — it is a generated,merge=os-regenartefact and none of those three touches the D3 entry this diff edits, so the contention is the one the regen driver exists for rather than two hands on the same prose.Generated by Claude Code