Repository navigation
spec: three published prescriptions assert the confirmation gate is NOT performed — PR #17486 makes all three false, and each one actively tells an author the flag stops nothing #17487
Description
Summary
@objectstack/spec carries three customer-facing prescriptions that state, in the present tense, that the runtime confirmation door has not shipped. PR #17486 (issue #15942) ships it. The moment that PR merges, all three are false — and they fail in the direction that matters: they tell an author the safety flag stops nothing, so an author who reads them concludes the gate is inert and arranges a human in the loop some other way, or stops setting the flag at all.
This is the mirror image of the defect #15942 was filed for. That card was "published prose promises a pause the runtime does not perform". This is "published prose denies a pause the runtime now performs". Same class, same surface, opposite sign.
The three sites
All three are the shipped text, not comments: the first reaches consumers as a parse-error prescription on a .strict() schema, the other two are what spec-changes.json, the upgrade guide and os migrate meta project.
1. packages/spec/src/ai/tool.zod.ts — the requiresConfirmation entry of the retired-key guidance map:
Read this before you rely on it: 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.
2. packages/spec/src/migrations/entries/semantic/17.tool-requires-confirmation-retired.ts, replacement:
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
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. Until then the declaration is a contract and the human in the loop is still yours to arrange
Site 3 is the sharpest: after #17486 it instructs an author not to run the one experiment that would now demonstrate the gate working, on the grounds that the experiment would execute the destructive operation. It would not. It would be refused.
Why this is a card and not a rider on #17486
The spec half was declared read-only for the runtime half's executor, so #17486 does not touch packages/spec. That boundary is right — but the tense promotion was explicitly assigned to the landing change by the spec half itself. tool.zod.ts carries this comment immediately above site 1:
Tense follows enforcement: promote it in the change that lands the door, not before.
So the work is declared, the trigger has arrived, and the only thing missing is a carrier. Filing it rather than riding it also keeps the two regeneration gates (check:spec-changes, check:upgrade-guide) and the tombstone's own text on a diff a domain:spec seat owns.
What the fix is
Promote the tense at all three sites and drop the "not yet" hedges. The substance to keep, all of it already true after #17486:
- the refusal is
ACTION_CONFIRMATION_REQUIRED, 428, naming the action and the member to set; - it is a GATE, not a queue — nothing is parked, nothing is held, and a refused call simply did not run;
- the enforced set is the doors that enforce
ai.exposed— todayinvokeBusinessActionvia MCPrun_action. REST/actionsis notai.exposed-gated and sits outside the gate, so an author relying on it for an API-key agent must still arrange their own human; - only the author's declared
ai.requiresConfirmation === truerefuses; the widerlist_actionsheuristic advises but never refuses; confirm: trueis an unverifiable caller claim. The gate makes forgetting loud; it does not prove a human.
Regenerate after the edit: pnpm --filter @objectstack/spec check:generated --fix covers check:spec-changes and check:upgrade-guide (site 1 is hand-written text with no generator).
Blocked-by: #17486 — the text may only be promoted once the door is on main, or it becomes false in the original direction instead.
Ordering hazard, restated
The domain:spec seat flagged the symmetric hazard on #15942 (comment 5567321513): a release cut between the two PRs. It repaired the wording to be contract-referential rather than relying on ordering, which is why nothing was broken while #15942 sat blocked. That repair is exactly what now needs its second half — the hedges it added are load-bearing only until the door lands, and permanent thereafter only as a defect.
Evidence
Read on origin/main at merge base c4d1759fa. Positive control on the same greps: requiresConfirmation lights 10 times across packages/spec/src, so the three readings above are a measurement and not a mis-aimed pattern.
Filed from the #15942 runtime half (PR #17486) as an out-of-scope finding: packages/spec was read-only for that delivery, and this seat did not edit it.
Blocked-by: #15942
Generated by Claude Code
Activity
os-litant commentedon Sep 10, 2026
Triage: lands in
packages/spec⇒domain:spec; typeBug,priority:p2,pm:blocked.pm:blocked, and the block is live — measured 2026-09-10T17:21:00Zdraft=true,mergeable_state=blocked— not mergedpm:dispatchedtoos-justin,priority:p1,security,needs:contract-review⇒ All three prescriptions are TRUE today — the runtime confirmation door has not shipped. They become false the moment #17486 merges. ⛔ Correcting them now would make the published text wrong in the other direction, and in the more dangerous direction: it would promise a pause that does not yet exist.
Blocked-by: #15942recorded on the body — the issue, not the PR, so the unlock scan can fire when it closes.⭐ Why p2 rather than p3, unlike the other two docs-drift cards this fire
The other two mislead about a shape. This one will mislead about a safety gate, and the card names the direction precisely:
⇒ An author who stops setting
requiresConfirmationbecause the prose says it is inert loses the gate at the moment it starts working. And these are not comments — the first reaches consumers as a parse-error prescription on a.strict()schema, and the other two are whatspec-changes.json, the upgrade guide andos migrate metaproject. ⇒ Shipped text on a security-adjacent surface.⭐ The card's own framing is the sharpest statement of the class and I am keeping it: #15942 was 「published prose promises a pause the runtime does not perform」; this is 「published prose denies a pause the runtime now performs」. Same class, same surface, opposite sign.
security— is the card that carries the real exposure. This one is its residue and cannot bite until that one lands.On release
Triage seat ·
session_017VGfRocA8VjczSe84fgjY3· R+169 · 2026-09-10T17:22Z (timestamp taken in the same tool call that posts) · comment from the triage seatGenerated by Claude Code
os-litant commentedon Sep 10, 2026
⛔ Correction to the grading comment above —
findingwas left on this card by that write and is now stripped.findingmeans awaiting first grading, and the rule is 定级即离标 — it comes off in the same stroke as the grade. The card carried it when I read it live (my cached board copy showed no labels, which is why the removal was not in the write). ⇒ A grade and afindingon one card is a half-state, and it would have kept this card in the ungraded-finding sweep after it was graded.Labels now:
bug/domain:spec/pm:blocked/priority:p2. ⛔ Nothing else changed; the grading above stands as written.Triage seat ·
session_017VGfRocA8VjczSe84fgjY3· R+169 · 2026-09-10T17:23Z · comment from the triage seatGenerated by Claude Code
fix(pm): H19 judges a `Blocked-by:` block against the body's target s…
os-steve commentedon Sep 20, 2026
Claim: PM loop round 13
Session:
session_01AmH9bKvGoLjiY86Q4Z3og2Branch:
claude/issue-17487-confirmation-gate-prescriptionsWorktree:
objectstack-issue-17487Domain:
domain:specSeat:
domain:spec#4File surface:
packages/spec/src/ai/plus whichever of the two remaining prescription sites the taker re-derives (thespec-changesentry, the upgrade guide, theos migrate metaprojection) — stop on breach; explain in the reportContainer & model:
M,mode:subagent,model: opus(tier derivation quoted below — no path-derived mandate)Clause-②: no
Thread-read: 5731153371 (triage's unblock, 2026-09-18T14:06Z) — the newest comment on this card at the moment this claim is written.
Serial constraints cleared: none — see the occupancy reading in prose below (⛔ not inside a span, so its stamp substitutes).
Occupancy scan taken in this act at 2026-09-20T01:45Z over all 21 open PRs (314 distinct held paths, first files page per PR):⚠️ Radius: the first files page (100 entries) of each open PR; a PR with more than 100 changed files is under-read and an unopened branch is invisible by construction. ⚠️ The two remaining sites are not named above because this card's own sites have to be re-derived first — the taker re-runs this scan against whatever paths the re-derivation names, before editing them.
packages/spec/src/ai/tool.zod.tsreads FREE. ⭐ Firing controls in the same scan:packages/spec/src/ui/component.zod.tsreads HELD by #19219 (this seat's own PR, currently in the merge queue) andpackages/spec/src/ui/view.test.tsreads HELD by #19226 ⇒ the zero is a reading, not a dead probe. Dark control: a nonexistent path reads 0.Why
Clause-②: noNothing here moves an accept set or a published export surface. The three artefacts are prose that describes runtime behaviour, and the behaviour they describe already changed when #15942's door shipped. Feed the same authored metadata in before and after this card: accepted stays accepted, refused stays refused, not one byte moves. ⇒ 辅助面, and the second cut answers 「它误导了 AI 吗」 with yes — see below.
dispatch-gates --tierflagspackages/spec/src/**as a Clause-② SUSPECT surface. That is a floor, never a clearance, and this seat judges it from the card CONTENT: the diff is docblock and prose. If the taker finds it must move a schema, a default or an exported name to make the prose true, that is a different card's shape — stop and report, ⛔ do not re-declare the arm unilaterally.Why this card is picked, and why it is not a p2 the North Star bars
docs/NORTH-STAR.md「优先级」 item 3 bars p2/p3 工具卡、契约卫生卡 while product P0/P1 are open. This is neither: it is the North Star's own item 4 — 「写给 AI 的文档与 skills 说错一句,等于产品缺陷」 — in its sharpest form. Three published, customer-facing prescriptions state in the present tense that the confirmation door has not shipped. It has. An author who reads them concludes the safety flag stops nothing and either arranges a human in the loop some other way or stops setting the flag. ⇒ the failure direction is the dangerous one, and the victim is the reader who believed the protocol.Tier derivation, quoted from this act's own run
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/spec/src/ai/tool.zod.ts(exit 0), verbatim:Triage's unblock (
5731153371) verified site 1 only, readingpackages/spec/src/ai/tool.zod.tsonorigin/mainand quoting it verbatim: 「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 …」. It wrote, in its own words, that the other two sites are ⛔ 未测.⇒ the taker re-reads all three on today's
origin/mainbefore writing a line, and reports any site whose text is already correct as NOT A DEFECT rather than editing it to match a prediction.Generated by Claude Code
Generated by Claude Code
os-steve commentedon Sep 20, 2026
os-dev-report
{ "issue": 17487, "status": "done", "branch": "claude/issue-17487-confirmation-gate-prescriptions", "pr": "https://github.com/objectstack-ai/objectstack/pull/19234", "session": "session_01AmH9bKvGoLjiY86Q4Z3og2", "premise_still_valid": true, "summary": "Re-derived all three prescription sites on today's origin/main (merge base 805811e0d) and every one is FALSE today: packages/spec/src/ai/tool.zod.ts (TOOL_RETIRED_KEY_GUIDANCE.requiresConfirmation), plus the replacement and acceptanceCriteria fields of packages/spec/src/migrations/entries/semantic/17.tool-requires-confirmation-retired.ts. CORRECTION to the card's count of carriers: the spec-changes entry, the upgrade guide and the os migrate meta projection are NOT three separate sites -- they are three generated projections of the ONE ADR-0087 D3 entry file, so the measurement is three false prescriptions living in TWO source files, plus three generated artefacts (src/migrations/registry.ts, spec-changes.json, docs/protocol-upgrade-guide.md) regenerated here by check:generated --fix. Each prescription now states the refusal in the present tense, held to the door that runs (ACTION_CONFIRMATION_REQUIRED / 428 / the confirm member; a GATE not a queue; nothing read and nothing written on a refusal, because the gate sits before loadActionSubjectRecord) AND carries the door's two BOUNDS so it cannot over-promise: the enforced set is the ai.exposed doors, today the action door reached from MCP run_action, with REST /actions outside it, and the confirmation member is an unverifiable caller claim. A new pin, packages/spec/src/ai/tool-confirmation-prescription-tense.pin.test.ts (registered in vitest.repo-tests.json), ties the three shipped strings to actionConfirmationRefusal and to invokeBusinessAction so neither half can move alone -- the tie that was missing the first time. NOT A DEFECT, read and left alone: packages/spec/src/contracts/ai-service.ts (already normative present tense) and content/docs/ai/tools.mdx (about the retired TOOL-level key, which has not returned). NO pin was moved: ui/action-requires-confirmation-docblock.pin.test.ts was read and it anchors on ui/action.zod.ts and actionLooksDestructive, neither of which this diff touches, so it covers none of the three sites.", "tests": "ALL exit codes captured BEFORE any pipe (cmd > log 2>&1; EXIT=$?). | pnpm --filter @objectstack/spec test -> exit 0: \"Test Files 499 passed (499) / Tests 14614 passed (14614)\". | pnpm --filter @objectstack/spec test:repo -> exit 0: \"Test Files 34 passed (34) / Tests 580 passed (580)\". | pnpm --filter @objectstack/spec typecheck -> exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck, the last printing \"OK -- @objectstack/spec’s test layer compiles under packages/spec/tsconfig.test.json\"). | The new pin alone, vitest --project repo -> exit 0, 8/8. | pnpm --filter @objectstack/spec check:generated -> exit 0: \"All 16 generated artifacts are up to date.\" | GATE FAMILIES: all 85 commands derived by scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands were run locally and all exit 0. Eight first returned NOT MEASURED rather than a finding: check:api-surface, check:api-surface-declarations, check:browser-reachable-entries, check:dual-source-exports, check:entry-nameability, check:exported-any and check:generated against a stale packages/spec/dist (exit 1, \"recorded 900d73e468... packages/spec/src now hashes to 2fe88c9d2e...\"), and check:doc-formula-expressions at exit 3 PREREQUISITE NOT MET -- all re-run green after building spec, formula and lint. Three more (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt) returned exit 3 PREREQUISITE NOT MET for want of a full dist closure, and re-ran green after turbo run build over ./packages/* and ./packages/*/* (72 tasks, 4m04s). | pnpm lint (eslint . --no-inline-config, WHOLE REPO, no narrowing claimed) -> exit 0 at head fe911445a6, 72s on the shared box. | NOT MEASURED locally and left to CI, named: the five path-scheduled ci.yml jobs (Test Core, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core), the 11 wide-population families, the 49 artifact-roster families and the 5 workflow-valued commands -- all outside the 85 by dispatch-gates' own accounting. | ABLATION (reverse verification): fix committed FIRST; each leg mutated on disk through scripts/ablation-replace.mjs (anchor hit declared, blob hash proven to move), direction predicted before the run, then restored and proven by blob hash == HEAD with git diff HEAD empty (whole-tree diff byte count 0). | leg 1 -- re-insert \"The refusal is DECLARED, not yet performed\" into the D3 entry replacement. PREDICTED RED on \"no shipped prescription denies the refusal\". OBSERVED RED: \"ADR-0087 D3 entry -- replacement denies a door that exists: expected [ 'DECLARED, not yet performed' ] to deeply equal []\". blob 76b6f3cf137f -> a2c25624a4f1, restored to 76b6f3cf137f. | leg 2 -- rename the gate call inside invokeBusinessAction. PREDICTED RED on \"the AI-facing door still calls the gate pre-dispatch\". OBSERVED RED. blob c9b6c637e77d -> fcc26c7400b3, restored to c9b6c637e77d. | leg 3 -- make the REST /actions door name the gate. PREDICTED RED on the over-claim guard. OBSERVED RED. blob 5aa9ba6dd77f -> baeccba3d35e, restored to 5aa9ba6dd77f. WARNING: leg 3’s FIRST attempt was a NO-OP and is reported as such rather than quietly retried -- the replacement text still contained the anchor, so ablation-replace refused (\"the anchor count moved 1 -> 1, a drop of 0, not the declared 1\") and the test never ran; the reading above is the re-anchored re-run. | Recovery run after every leg: 8/8 green. | CLAUSE-2 CARRIER CHECK: PM_SWEEP_REPO=objectstack-ai/objectstack node scripts/pm/check-clause2-carriers.mjs --pair 19234 -> exit 4 (EXIT_PAIR_ADVERSE), captured before any pipe. The adverse row is C6 and is NOT a clause-2 breach: both limbs read DECLARED no (card 17487 and the PR body). C6 says the domain:spec lane owes a \"## Contract review\" record naming head fe911445a6 with a Reviewed-by line, and none exists. That record is the SEAT’s to post, the script is explicitly report-only, and the PR is correctly left draft and out of the queue meanwhile. | CLAUSE-2 EVIDENCE that no accept set moved: check:authorable-surface and check:api-surface both green with ZERO diff under packages/spec/authorable-surface/ and packages/spec/api-surface/. The pin’s last case feeds the same authored metadata before and after -- tool.requiresConfirmation still REFUSED, a minimal tool still ACCEPTED, action.ai.requiresConfirmation still ACCEPTED for both true and false. | CHANGESET OWED AND PRESENT (@objectstack/spec, patch), judged by measurement not assumption: the new sentence \"unverifiable caller claim\" resolves in 18 built files under packages/spec/dist (published via the files[] entry \"dist\"); the retired hedge \"not yet the behaviour\" now resolves in 0; positive control \"the silent-strip class\" resolves in 18; and spec-changes.json, also in files[], moved bytes. | OCCUPANCY re-scan at 2026-09-20T01:52Z over ALL 21 open PRs (314 distinct held paths): 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. RADIUS GAP CLOSED: PR #17076 was under-read on page 1, so all 7 pages (639 files) were fetched and it holds none of my paths. 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, not two hands on one piece of prose.", "mcp_calls": "0 -- no MCP GitHub tool was called at all; every GitHub read and write went through the REST proxy with curl.", "api_writes": "2 -- POST /repos/objectstack-ai/objectstack/pulls (draft PR 19234; body read back in full and byte-identical except the platform trimming one trailing newline, one session-URL footer stored, zero angle brackets stored, all eight section headings intact) and POST /repos/objectstack-ai/objectstack/issues/17487/comments (this report). ZERO label writes: the dispatch reserved the dual-carrier gate for the seat. Outside the REST budget: five git pushes on the feature branch, no force-push, main never touched.", "open_questions": [], "out_of_scope_findings": [ "to file (class b, contradicts a contract @objectstack/spec/contracts declares; dedupe words: MCP_GUIDE, requiresConfirmation, HITL approval queue, nothing server-side pauses, confirmation gate): packages/spec/docs/MCP_GUIDE.md section 3 Side Effects tells an author to gate side effects with ai.requiresConfirmation on the underlying ACTION \"(+ the HITL approval queue)\", and the block immediately below warns that \"nothing server-side pauses on it\". The warning is correctly scoped to the MCP capability descriptor in that page's own 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 point in this card's dangerous direction. Outside the claim's declared file surface, so not edited here. Note the file is NOT in packages/spec package.json files[], so it publishes nothing.", "to file (class b, same contract; dedupe words: actions-as-tools, human-in-the-loop, protocol boundary, run_action, confirmation gate): content/docs/ai/actions-as-tools.mdx, section Human-in-the-loop approval, 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 with no mention of the confirmation gate that now sits between the ADR-0104 param contract and loadActionSubjectRecord. An omission on a security surface rather than a denial, which is why it is a separate card and not a rider here.", "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 is provenance prose about that row's split registration, not a prescription an author acts on, and it misleads nobody about the gate. Successor: the next change that touches that ledger row.", "noted, not filed: this PR touches NO governed surface (no docs/adr, no docs/NORTH-STAR.md, no .claude, no skills, no AGENTS.md, no CLAUDE.md), so no Chinese maintainer-readout section is owed on the body and Prime Directive #14 does not gate its landing. Successor: none needed." ] }Generated by Claude Code
os-steve commentedon Sep 20, 2026
Release: landed — PR #19234 merged through the merge queue. Seat
domain:spec#4,session_01AmH9bKvGoLjiY86Q4Z3og2.pm:dispatchedstripped and the assignee cleared in the same stroke; the card isclosed/completedby the PR's closing keyword.The landing, probed rather than trusted
⛔ Not read off the API's
mergedfield. Measured on a fetchedorigin/mainat 2026-09-20T03:49Z:main9059a9472a61a12dfc626df51d86df49de87562d—fix(spec): state the confirmation gate in the present tense …(#19234)mainthat contract is ENFORCED— ×1mainnot yet the behaviour— ×0⭐ Firing control on the same grep:
(#19219)reads 1 onmain. Dark control:(#99999999)reads 0.What landed, and the correction the round produced
Three published prescriptions that denied a door that exists now state the refusal in the present tense —
ACTION_CONFIRMATION_REQUIRED(428), a GATE and not a queue, a refused call having read and written nothing.⭐ The implementer corrected this card's own count and it is worth recording: the three prescriptions do not live in three files. They live in two —
packages/spec/src/ai/tool.zod.tsand the ADR-0087 D3 entry — because thespec-changesentry, the upgrade guide and theos migrate metaprojection are three generated projections of that one entry. The reviewer verified the projection chain structurally. Two further sites were read and left alone as NOT A DEFECT (contracts/ai-service.tsalready normative;content/docs/ai/tools.mdxspeaks of the retired tool-level key).⭐ The reason this round needed a contract review even at
Clause-②: noThe fix's own failure mode is this card's defect with the sign flipped: prose claiming more protection than the door delivers. The shipped text therefore states the door's two BOUNDS — the enforced set is the
ai.exposeddoors (today the action door reached from MCPrun_action, with REST/actionsoutside it), andconfirm: trueis an unverifiable caller claim, so 「the gate makes FORGETTING loud, it does not prove a human」. The reviewer re-derived every claim againstpackages/runtime/src/action-execution.tsand confirmed the gate sits beforeloadActionSubjectRecord, that the REST door is ungated, and that only the booleantrueconfirms.Review of record: PR #19234 comment
5747239069—Served-tier: CONTRACT_REVIEW_TIER, VERDICT: PASS, no blocking findings. It ran four ablation legs itself, one of them (ENFORCED→DECLARED, not yet performed) on a carrier the implementer had not ablated.Clause-②: noheld on measurement: ten authored inputs fed through the base and head builds give identical structural outcomes;authorable-surface/andapi-surface/are absent from the diff because nothing moved, andcheck:generatedis green.Carried out of this round with no card
Two of the review's noted findings are card-shaped and have no issue number:⚠️ With the four already carried from the #17987 round, six findings now have no carrier — this seat cannot open issues in this session, and the gap is on the maintainer's desk.
content/docs/ai/actions-as-tools.mdx's human-in-the-loop section omits the server-side refusal, andpackages/spec/docs/MCP_GUIDE.md§3 pairs an approval-queue requirement with 「nothing server-side pauses on it」.Generated by Claude Code
fix(spec): state the confirmation gate in the present tense — three s…
feat(scripts): report-only present-tense phrase reading on the spec d…
test(spec): security, ai, identity, integration, migrations, marketpl…