Skip to content

fix(spec): state the confirmation gate in the present tense — three shipped prescriptions were denying a door that exists - #19234

Merged
os-steve merged 4 commits into
mainfrom
claude/issue-17487-confirmation-gate-prescriptions
Sep 20, 2026
Merged

os-steve merged 4 commits into
mainfrom
claude/issue-17487-confirmation-gate-prescriptions

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #17487

Clause-②: no

The defect, and its direction

Three shipped, customer-facing prescriptions in @objectstack/spec stated in the present tense that the runtime confirmation door had not shipped. It has: actionConfirmationRefusal is called pre-dispatch by invokeBusinessAction in @objectstack/runtime, and the MCP run_action tool grew the confirm member in the same change (the card behind it, #15942, is done — state_reason=completed; its changeset action-confirmation-gate-enforced is still pending, so the door is on main and 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/main

Triage's unblock comment verified site 1 only and said the other two were unmeasured. All three were re-read at merge base 805811e0d.

# Path Current text Verdict
1 packages/spec/src/ai/tool.zod.tsTOOL_RETIRED_KEY_GUIDANCE.requiresConfirmation "the declaration is the contract, not yet the behaviour — the runtime door that performs the refusal ships separately, and until it does, setting the flag does NOT stop an unconfirmed call. Do not try to verify the gate by invoking the operation without the member: until that door lands, such a call simply RUNS." FALSE today
2 packages/spec/src/migrations/entries/semantic/17.tool-requires-confirmation-retired.tsreplacement "The refusal is DECLARED, not yet performed — the runtime door lands in #15942, so until then the flag stops nothing on its own and the human in the loop is still yours to arrange" FALSE today
3 the same file — acceptanceCriteria "Do NOT try to 'prove the gate' by invoking the operation without the confirmation member: the runtime door that refuses lands in #15942, so before that ships the call is not refused, it RUNS the destructive operation." FALSE today

Correction to the card's count of the carriers. The card names the spec-changes entry, the upgrade guide and the os migrate meta projection 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 by check: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), with requiresConfirmation lighting 10 files under packages/spec/src as the positive control. Two adjacent texts were read and left alone as NOT A DEFECT: packages/spec/src/contracts/ai-service.ts already states the gate in normative present tense, and content/docs/ai/tools.mdx says 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:

  • the refusal is ACTION_CONFIRMATION_REQUIRED, 428, naming the action and the member confirm: true;
  • a GATE, not a queue — nothing is parked, and a refused call did not run: the gate sits before loadActionSubjectRecord, so no record is read and none written;
  • the enforced set is the doors that enforce the author's ai.exposed opt-in — today the action door reached from MCP run_action. REST /actions is not ai.exposed-gated and sits outside the gate, so an API-key agent on that route still needs its own human;
  • only the author's declared ai.requiresConfirmation: true refuses, and only the boolean true confirms; the wider list_actions heuristic advises and never refuses;
  • confirm: true is 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.ts is 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.ts was read: it anchors on the ai.requiresConfirmation JSDoc in ui/action.zod.ts and on actionLooksDestructive, 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-surface and check:api-surface are green with zero diff under packages/spec/authorable-surface/ and packages/spec/api-surface/. The pin's last case feeds the same authored metadata in before and after: tool.requiresConfirmation still refused, a minimal tool still accepted, action.ai.requiresConfirmation still accepted for both true and false. What moved is string content inside dist and spec-changes.json, which is why a patch changeset 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 against HEAD with git diff HEAD empty:

leg mutation predicted observed
1 re-insert The refusal is DECLARED, not yet performed into the D3 entry's replacement RED on "no shipped prescription denies the refusal" RED, naming the replacement carrier
2 rename the gate call inside invokeBusinessAction RED on "the AI-facing door still calls the gate pre-dispatch" RED
3 make the REST /actions door name the gate RED on the over-claim guard RED

Leg 3's first attempt was a no-op and is reported as such: the replacement text still contained the anchor, so ablation-replace refused (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 --commands for this diff were run locally and exit 0, exit codes captured before any pipe. Eight first returned a stale-dist or PREREQUISITE NOT MET result (exit 1 / exit 3 — not measured, not findings); they were re-run green after pnpm --filter @objectstack/spec build and a full turbo run build closure. pnpm lint (eslint . --no-inline-config, whole repo, no narrowing) exits 0 at HEAD. 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:

  1. packages/spec/docs/MCP_GUIDE.md (around the "Side Effects" section) tells an author to gate side effects with "ai.requiresConfirmation on 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.
  2. 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/contracts declares. 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.ts says of the ACTION_CONFIRMATION_REQUIRED row 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.json and src/ai/tool.test.ts all read FREE. Firing controls in the same scan: packages/spec/src/ui/component.zod.ts HELD by #19219, packages/spec/src/ui/view.test.ts HELD by #19226; dark control (a nonexistent path) reads FREE. One reading to flag: packages/spec/src/migrations/registry.ts reads HELD by #19223, #19090 and #18319 — it is a generated, merge=os-regen artefact 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

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ai tests tooling labels Sep 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

3 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 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/protocol/objectui/actions.mdx (via requiresConfirmation (symbol, a field of const object TOOL_RETIRED_KEY_GUIDANCE))
What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/spec-changes.json, packages/spec/vitest.repo-tests.json) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

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 e233db9dbb9c43816b1f2c44a3381c96402334a3packageMentionDocs.

Which tree this was computed on

This run read content/docs from 19bc03098b5427106ab5ff3b99b7521d21b1cfe1 — the merge of head fe911445a65c84611f52f24b96a2cfa0bcaa78df into base e233db9dbb9c43816b1f2c44a3381c96402334a3, 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 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

⚠️ 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 e233db9dbb9c43816b1f2c44a3381c96402334a3 → 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

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fe911445a65c84611f52f24b96a2cfa0bcaa78df

Contract-tier review of record for PR #19234 (card #17487, domain:spec, Clause-②: no). Every reading below was re-derived from the tree at the head above, on a detached worktree (merge base 805811e0df87), never from the PR body or the dev report; the two narrations were read only to know what to falsify. Exit codes were captured before any pipe. Nothing was pushed, labelled or edited on the PR.

① Derived judgments

1. The over-promise question — each claim in the three shipped strings, re-derived against the runtime (packages/runtime/src/action-execution.ts at this head, line numbers from it).

shipped claim what the tree says reading
refused with ACTION_CONFIRMATION_REQUIRED (428) ACTION_CONFIRMATION_REQUIRED_CODE = 'ACTION_CONFIRMATION_REQUIRED' (l.1053) and ACTION_CONFIRMATION_REQUIRED_STATUS = 428 (l.1061); the code is a registered row of ERROR_CODE_LEDGER (packages/spec/src/api/error-code-ledger.zod.ts l.380); invokeBusinessAction throws it with code, status and details (l.2026-2034), and the MCP bridge's errorResultFromThrown (packages/mcp/src/mcp-http-tools.ts l.288) forwards all three into the tool error delivered
the refusal names the action and the exact member to set details = { actionName, objectName?, confirmationMember: AI_ACTION_CONFIRMATION_MEMBER } (l.1084-1090); AI_ACTION_CONFIRMATION_MEMBER = 'confirm' (packages/spec/src/contracts/ai-service.ts l.237) — the member is echoed off the constant, not hand-spelled delivered
a GATE, not a queue; a refused call did not run — no record was read and none was written in invokeBusinessAction the gate call (l.2025) sits AFTER enforceActionParams and BEFORE loadActionSubjectRecord (l.2037), ahead of the declarative-update, flow and script branches. The steps before it read metadata (getMeta, resolveActionByName) and the activation ledger's in-memory projection (ql.isActionEnabled, synchronous, packages/objectql/src/engine.ts l.3882) — no data record. action-confirmation-gate.test.ts at this head pins "refuses with the ADR-0112 envelope, dispatches nothing, and reads no record" and "gates a FLOW action too — ahead of the type branch, so no run is created": 9/9, exit 0 delivered
bound (1a): the door reached from MCP run_action is gated the run_action handler (mcp-http-tools.ts l.1038-1133) declares confirm as a closed optional boolean on its input schema and forwards it to bridge.runAction, which is actionExec.invokeBusinessAction (packages/runtime/src/domains/mcp.ts l.727) — the ONLY non-test caller of invokeBusinessAction under packages/ delivered
bound (1b): REST /actions is NOT ai.exposed-gated and sits OUTSIDE the gate packages/runtime/src/domains/actions.ts (1037 lines) carries 0 hits for actionConfirmationRefusal, ACTION_CONFIRMATION_REQUIRED and exposed. Radius: that one file, three literal patterns. Controls: the same patterns hit action-execution.ts at l.1071/1053/519, and ablation leg C below made this very file fire the pin. Enumeration of the set the sentence names: actionAiExposureError( has exactly two call sites (domains/mcp.ts l.700, the listing; action-execution.ts l.1973, invokeBusinessAction); ai.exposed is read at ONE non-spec, non-test, non-generated site (l.519); packages/services/service-ai has no action bridge at all. So "the doors that enforce the author's ai.exposed opt-in" is a set of exactly one door in this repo, and it is the gated one delivered
bound (2): only the author's DECLARED true refuses; only the boolean true confirms; the listing heuristic advises and never refuses; an explicit false never refuses gate body: if (action?.ai?.requiresConfirmation !== true) return undefined; then if (request?.[AI_ACTION_CONFIRMATION_MEMBER] === true) return undefined; (l.1079, l.1081). The heuristic actionLooksDestructive has exactly one call site — summarizeAction l.1133, the list_actions summary. Runtime cases "admits ONLY the boolean true — a truthy string and false are not attestations", "does NOT refuse an undeclared action the listing calls destructive" and "honours an explicit false on a danger-variant action" pass at this head; MCP case "is a CLOSED boolean — a truthy string is refused by the door" passes (mcp-action-confirmation-member.test.ts 6/6, exit 0) delivered
confirm: true is an unverifiable caller claim — the gate makes forgetting loud, it does not prove a human nothing in the gate, the bridge or the tool handler verifies the attestation; the door's own docblock (l.1036-1040) states the same limit delivered

⇒ 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.

  • The diff's prose edits are exactly the three strings: TOOL_RETIRED_KEY_GUIDANCE.requiresConfirmation in packages/spec/src/ai/tool.zod.ts (the other hunk in that file is comment-only), and replacement + acceptanceCriteria of packages/spec/src/migrations/entries/semantic/17.tool-requires-confirmation-retired.ts; reason is untouched (past tense about the tool-level key, correctly left alone).
  • Denial sweep at this head over *.ts *.md *.mdx *.json *.mjs (excluding node_modules, dist, .git, .changeset, CHANGELOG.md) for eight retired phrasings — not yet performed, ships separately, simply RUNS, stops nothing on its own, does NOT stop an unconfirmed, lands in #15942, not yet the behaviour, until that door lands — plus five widening ones (yours to arrange, nothing server-side, no pause, HITL approval queue, stops nothing): the only hits about this gate are the PR's own comment in tool.zod.ts and the pin's verbatim self-test sentences. Control: the identical sweep against the merge base fires on tool.zod.ts (3), the D3 entry (2), spec-changes.json (2) and docs/protocol-upgrade-guide.md (2). Out-of-surface hits are listed under ③.
  • The corrected count is right, verified structurally: src/migrations/registry.ts carries a generated region concatenated from src/migrations/entries/ by scripts/build-migration-registry.ts (its header says so and check:migration-registry proves it); src/migrations/spec-changes.ts composes spec-changes.json from MIGRATIONS_BY_MAJOR (l.39, l.251); scripts/build-upgrade-guide.ts reads MIGRATIONS_BY_MAJOR (l.24); and os migrate meta (packages/cli/src/commands/migrate/meta.ts l.8-15) imports applyMetaMigrations from @objectstack/spec, whose chain.ts (l.18) reads the same table. One entry, three projections — three prescriptions in two source files is the correct measurement.
  • The two NOT A DEFECT sites hold: packages/spec/src/contracts/ai-service.ts carries no hedge (grep for not yet, ships separately, until, lands in, #15942: the only until is l.405, about the chat loop) and states the refusal in normative present tense at l.290-297; content/docs/ai/tools.mdx l.109-112 and l.151-155 speak only of the retired TOOL-level key, which "returns only together with its enforcement" — still true.
  • Adjacent surfaces read and consistent: packages/mcp/src/skill-md.ts l.166-170 (already present tense: "the server REFUSES such a call without it"), packages/runtime/src/http-dispatcher.ts l.1208-1215, skills/objectstack-ai/SKILL.md l.311-317.

3. Clause-②: no holds — measured, not inferred.

  • Feed test: the same ten authored inputs parsed through the merge-base dist and the head dist (packages/spec/dist/ai + dist/ui, both freshly built): ToolSchema × minimal, requiresConfirmation: true, requiresConfirmation: false, an unknown key, a missing description; ActionSchema × plain, ai.requiresConfirmation true, false, the string 'true', and ai.exposed. Structural outcomes (success flag, issue paths, issue codes) are IDENTICAL across the two dists for all ten. Only the two tool.requiresConfirmation cases differ, and only in message text: the base message carries "not yet the behaviour", the head message carries ACTION_CONFIRMATION_REQUIRED (428) and REST /actions. Accepted stays accepted, refused stays refused.
  • packages/spec/authorable-surface/ and packages/spec/api-surface/ are absent from the diff because nothing moved, not because a regeneration was skipped: after a fresh pnpm --filter @objectstack/spec build at this head (build input hash 2fe88c9d2e04d903…), check:generated exits 0 with "All 16 generated artifacts are up to date", check:authorable-surface and check:api-surface among them — each of those gates regenerates and compares against the committed shards.

4. The pin is a real pin — four legs run here, direction predicted before each run, every mutation and restore proven by blob. src/ai/tool-confirmation-prescription-tense.pin.test.ts under vitest project repo (registered in vitest.repo-tests.json; check:cross-package-test-inputs exit 0 sees its escaping reads). Unmutated: 8/8, exit 0.

leg file / mutation (anchor hit exactly once) predicted observed blob moved → restored
A spec/src/ai/tool.zod.ts: "contract is ENFORCED. An AI-facing" → "contract is DECLARED, not yet performed. An AI-facing" — the ToolSchema carrier, which the dev's legs did not ablate 1 red on "no shipped prescription denies the refusal" 1 failed / 7 passed: "ToolSchema rejection (TOOL_RETIRED_KEY_GUIDANCE.requiresConfirmation) denies a door that exists: expected [ 'DECLARED, not yet performed' ] to deeply equal []" 66110127bebd9572832b451c66110127bebd = HEAD
B runtime/src/action-execution.ts: gate predicate !== true=== undefined (a widening onto undeclared actions) 1 red on "still reads the DECLARED flag" 1 failed / 7 passed: "the gate stopped reading the DECLARED flag" c9b6c637e77dd499e48abc1fc9b6c637e77d = HEAD
C runtime/src/domains/actions.ts: a comment made to name actionConfirmationRefusal (replacement chosen so the anchor text disappears — the shape the dev's first attempt got wrong) 1 red on the over-claim guard 1 failed / 7 passed: "the REST actions door now enforces the confirmation gate — the ai.exposed bound in the shipped prescriptions is stale" 5aa9ba6dd77f7eb2cc99b8d25aa9ba6dd77f = HEAD
D runtime/src/action-execution.ts: ACTION_CONFIRMATION_REQUIRED_STATUS = 428;= 409; 1 red on the status match 1 failed / 7 passed on ACTION_CONFIRMATION_REQUIRED_STATUS =… c9b6c637e77d7d432a47f4c0c9b6c637e77d = HEAD

Each leg ran through scripts/ablation-replace.mjs in WRAP mode (anchor 1 → 0, blob proven to change, restore proven blob == HEAD with git diff HEAD empty); git status --porcelain read 0 lines after every leg; recovery run on the restored tree 8/8, exit 0. On the dev's reported legs: their three "from" blobs (76b6f3cf13, c9b6c637e77d, 5aa9ba6dd77f) are exactly the HEAD blobs of the D3 entry, action-execution.ts and domains/actions.ts at this head, so those readings were taken against this tree; the reported NO-OP on leg 3's first attempt is the tool's anchor-drop refusal (exit 3, nothing runs), and its re-run reading is reproduced by leg C here.

5. The generated projections are regeneration output. With the spec built at this head, check:generated (exit 0) reports check:migration-registry (src/migrations/registry.ts generated regions from entries/), check:spec-changes (spec-changes.json) and check:upgrade-guide (docs/protocol-upgrade-guide.md) all up to date — every gate in that wrapper regenerates and byte-compares against the committed file, so the three diff hunks are what the generators emit from the edited entry, not hand-typed. The spec-changes.json and upgrade-guide hunks change only the replacement and acceptance strings of the tool-requires-confirmation-retired entry, consistent with the single-entry edit.

6. The changeset. .changeset/17487-confirmation-gate-prescriptions-present-tense.md: @objectstack/spec: patch, Clause-②: no, no arm. Published bytes DO move: packages/spec/package.json files[] ships dist and spec-changes.json; at this head 18 built files under dist/ carry "unverifiable caller claim" and 0 carry "not yet the behaviour"; spec-changes.json carries the new text twice. No export moved (check:api-surface green, no shard diff) and no accept set moved (item 3), so patch is the right level for prose-only movement on shipped artefacts. The release-ordering claim holds at this head: .changeset/action-confirmation-gate-enforced.md (@objectstack/runtime: minor, @objectstack/mcp: minor) is still pending, so the door is on main and unreleased, and one changeset version run consumes both. The three ids the old strings carried (#15942) are gone from the shipped text; check:doc-authoring exits 0 (15803 customer-facing strings clean).

② Semver level

patch on @objectstack/spec — correct. What moves is the content of three shipped strings (parse-error prescription, D3 replacement, D3 acceptanceCriteria) and their generated projections; the accept set, the export surface and the authorable surface are byte-identical across the diff (measured in items 3 and 5). Clause-②: no is the right declaration and needs no arm.

③ Boundary flags

  • Governed surfaces: none touched — the file list is .changeset/…, docs/protocol-upgrade-guide.md and packages/spec/**; Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14 is not engaged. The landing waits only on this record (C6) for the domain:spec lane.
  • CI at this head: all 35 check runs completed; every conclusion is success or a path-filter skipped (Console Pin Gate, Build Docs, Packed-tarball smoke); the seven required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) all success.
  • Run here at this head, exit 0 each: the pin (8/8), action-confirmation-gate.test.ts (9/9), mcp-action-confirmation-member.test.ts (6/6), check:generated (16/16), check:cross-package-test-inputs, check:test-source-alias, check:doc-authoring, check:test-typecheck (spec), eslint on the three touched source files.
  • NOT MEASURED here: the full pnpm test / pnpm typecheck closure (CI's Test Core and TypeScript Type Check are green at this head and stand for it); an actual os migrate meta run (the projection is verified by import chain, not by execution); the docs-site build (Build Docs path-skipped).

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):

  1. content/docs/ai/actions-as-tools.mdx "Human-in-the-loop approval" (l.164-172) still places the open-MCP approval step at the protocol boundary and never mentions the server-side refusal; an omission on a customer-facing page, not a denial. The dev reported it for filing; an issue search on its dedupe words finds no card yet — the seat should file it.
  2. packages/spec/docs/MCP_GUIDE.md §3 (l.358-365) prescribes ai.requiresConfirmation on the action "(+ the HITL approval queue)" and warns "nothing server-side pauses on it" (scoped to the MCP capability descriptor, true of it). Not in files[], so it publishes nothing; not filed yet either.
  3. content/docs/releases/v17/17-4.mdx l.482-486 says "Setting the flag stops nothing until the runtime half lands" — RELEASE-OWNED and accurate for 17.4.0 as released; the release that consumes action-confirmation-gate-enforced and this changeset is where the flip belongs, never a rider here.
  4. Understatement, not over-claim: packages/spec/src/ui/action.zod.ts l.794-801 describes ai.requiresConfirmation as an "Override HITL confirmation for AI invocations" without naming the refusal it now triggers. Pinned by ui/action-requires-confirmation-docblock.pin.test.ts, outside this card's three sites; a successor edit.
  5. Precision on "no record was read": the steps ahead of the gate read metadata and an in-memory activation projection; the sentence is about the subject record and matches the contract's own wording (ai-service.ts l.295-296). Not a defect.
  6. packages/spec/src/api/error-code-ledger.zod.ts l.380 area keeps a forward-tense provenance note ("the door will assert this exact string by value"); provenance prose, not a prescription.

Implemented-by: claude/issue-17487-confirmation-gate-prescriptions
Reviewed-by: session_01AmH9bKvGoLjiY86Q4Z3og2

VERDICT: PASS

Contract-tier reviewer · session_01AmH9bKvGoLjiY86Q4Z3og2 · rendered 2026-09-20T03:10Z · every reading above was taken in this act against head fe911445a65c84611f52f24b96a2cfa0bcaa78df unless it names another tree


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review September 20, 2026 03:11
@os-steve
os-steve enabled auto-merge September 20, 2026 03:11
@os-steve
os-steve disabled auto-merge September 20, 2026 03:12
@os-steve
os-steve enabled auto-merge September 20, 2026 03:12
@os-steve
os-steve added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 9059a94 Sep 20, 2026
37 checks passed
@os-steve
os-steve deleted the claude/issue-17487-confirmation-gate-prescriptions branch September 20, 2026 03:35
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