fix(spec): refuse an auto-launched flow whose stack declares triggers without automation (#20332) - #20365
Conversation
… without automation (#20332) Every trigger plugin installs its trigger into the automation service at kernel:ready and installs nothing without it, and no runtime resolves `triggers` into `automation`. `validateTriggerCapability` now refuses `triggers` without `automation` on the same STACK_TRIGGER_CAPABILITY_REQUIRED code, prescribing `'automation'`; a stack declaring neither token is told to add both. `requires: ['automation']` keeps its message byte-for-byte. Docblocks, the two docs pages and the ledger comment stop saying one token installs the trigger; two test stacks that declared `triggers` alone for this refusal's sake now declare the pair. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…iggers-require-automation
… refused by os g, not "cannot run" `defineStack` now refuses a record-change flow whose stack declares `triggers` without `automation`, so `os g flow` into such a project stops the config from loading and is refused (exit 1, the tree byte-identical) instead of reporting "cannot run". The pin that held the old answer is flipped, and the CLI docs page and the pending os generate changeset stop describing that state. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…ange flow owes Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…iggers-require-automation
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 142 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 619bbd3a1e2f7242e4bf454cd6b081e52b860bf7 && git checkout 619bbd3a1e2f7242e4bf454cd6b081e52b860bf7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a88a1bb399ea1ba6d717b8f6fefa0b13ec3e66a8 cf6234716db87a1b16ceb8e3a9e7a8bc24bd14c2 && git checkout -B drift-repro a88a1bb399ea1ba6d717b8f6fefa0b13ec3e66a8 && git merge --no-ff cf6234716db87a1b16ceb8e3a9e7a8bc24bd14c2
node scripts/docs-audit/affected-docs.mjs --json a88a1bb399ea1ba6d717b8f6fefa0b13ec3e66a8
|
…t / os g text says either missing token stops the config loading The lint tier fixture's schedule flow owed only `triggers` before `defineStack` refused `triggers` without `automation`; it now declares the pair. The config comment `os init` writes, the flow header `os g flow` writes, and their docblocks no longer say a stack without `automation` loads the flow and never runs it: without either token the config is refused. Text only; no emitted code changes. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…iggers-require-automation
|
Confirmed: the deliberate correction of a pending release note. What was corrected
⛔ Do not restore the base text, and ⛔ do not apply The red check
|
|
Red check that is not this PR's:
|
Contract reviewServed-tier: PR #20365 (card #20332), draft, base ① Derived judgments1. The refusal is exactly the ruled class — RIGHT.
2. Every prescription, applied literally once, is accepted — RIGHT.
3. The runtime premise — RIGHT.
4. Census — RIGHT (0 producers).
5. Pins bite — RIGHT.
6. The lifted fences — RIGHT.
7. The deliberate changeset correction — RIGHT.
8. Changeset / semver — RIGHT.
9. CI at the head — WRONG as read at 2026-09-28T04:21Z, cause named; the merge-tree is clean.
10. Out of scope, named only — see ③. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #20332
Clause-②: no (narrowing)
BREAKING (
@objectstack/specminor): a published accept set narrows. No export, no error code and no accepted shape is added. The refusal reusesSTACK_TRIGGER_CAPABILITY_REQUIRED(status: 422).What changes
validateTriggerCapability(packages/spec/src/stack.zod.ts) refused an auto-launched flow only whenrequireslacked'triggers'. Its docblock said the trigger "is installed by ONE token,requires: ['triggers']". That is false. Every trigger plugin installs its trigger into the automation service atkernel:ready, and without that service it warns and installs nothing. No runtime's resolver turnstriggersintoautomation. So a stack withrequires: ['triggers']and arecord_changeflow passedos validate, booted, and never fired the flow.Triage direction
5861188312, verbatim: 「The contract choice, decided here: refuse, do not imply.」The refusal now has three arms, one line per offending flow, on the same code, header and
issuesshape. Each prescription is the whole fix for therequiresit was given:requires(auto-launched flow present)['automation', 'triggers']['automation']'triggers'['triggers']'automation'[]or absent['triggers']['automation', 'triggers']obsolete/invalidflowsThe refusal text, quoted (for review)
New arm,
requires: ['triggers']:Changed arm, neither token:
Unchanged arm,
requires: ['automation']:A choice made here: the neither-token message names both tokens
The dispatch left this open and suggested keeping today's message. Analysed on the four axes:
examples/app-showcase,examples/app-todo, theos inittemplate, andos g flow's own output. The CLI boot banner already prescribesrequires: ['automation', 'triggers']. No producer uses['triggers']alone.['triggers'], and the new arm would refuse them a second time. A prescription that leads to another refusal is a trap. The pineach prescription, applied literally once, is ACCEPTEDholds the property.Measurements (dispatch Zone 2)
§1 Reproduce on both runtimes.
if (hasTriggers) return errors;) built intopackages/spec/dist. The mutation was held byscripts/ablation-replace.mjsand confirmed indist/byscripts/ablation-dist-preflight.mjs. On a probe project withrequires: ['triggers']and onerecord_changeflow:os validateexited 0.os serve --devreachedServer is readyand printed⚠ Flows: 1 flow(s) declared but the automation engine is not enabled — they will never run. Add requires: ['automation', 'triggers'] to objectstack.config.ts.RecordChangeTriggerPlugin/ScheduleTriggerPlugin/TimeRelativeTriggerPlugin/ApiTriggerPlugin: automation service not available — … trigger NOT installed.os validateon the same probe exits 1 with the new message.origin/main96eb092, read only:packages/objectos-runtime/src/capability-loader.tsresolveCapabilityDependenciespullsqueue,jobandmessagingfortriggers, neverautomation. So the resolver does not imply it.apps/objectos/hosted-slate.tsHOSTED_FORCED_REQUIRES, andapps/objectos-eedefaultRequires. There a['triggers']stack runs, but so does a stack with norequiresat all. The published arm has refused that second stack since it landed. The refusal judges the stack's own declaration, which is portable, and not one host's floor.triggerspullsautomationin by itself") holds on neither resolver.§2 Which kinds need
automation. All four. The boot above printed all four "NOT installed" warns. Each plugin'skernel:readyhook resolvesautomationand returns if it is missing:packages/triggers/trigger-record-change/src/plugin.ts:47,trigger-schedule/src/plugin.ts:72,trigger-schedule/src/time-relative-plugin.ts:67,trigger-api/src/plugin.ts:62. No kind stays accepted.§3 Shape and door.
os validatereaches it throughloadConfig, which evaluates the author'sdefineStack()call (step 1), and not through its own stack parse (step 2). It is not a separate door.tsx bin/run-dev.js validate) at this branch:['triggers']exits 1,[]exits 1,['automation', 'triggers']exits 0.§4 Producer census (expected 0 producers; 0 found; three test stacks):
examples/**:app-showcaseandapp-tododeclare both.os validateexits 0 on each at this branch.app-crmdeclares['ui', 'automation']and has no auto-launched flow;os validateexits 0.app-multi-packageandembed-objectqldeclare norequires.packages/create-objectstack/src/templates/**:blankdeclares['automation']. It is out of this arm's reach.os init/os gon currentmain(PR fix(cli): generated scaffolds reach the stack, or os g says they do not (#20215) #20329 landed asc5dcb3ba07):initdeclares both.os g flowinto a['triggers']project was the "cannot run" answer and is now a refusal (see below).packages/**test stacks withtriggersalone and an auto-launched flow:packages/cli/test/generate-object-namespace-prefix.test.ts: converted to the pair.packages/qa/dogfood/test/fixtures/override-composite-fixture.ts: converted. Its boot mounts both explicitly;verify's harness does not readrequires.packages/lint/src/authoring-rule-input-tier.test.ts:93: not converted.packages/lintis fenced read-only for this dispatch. See "Owed, and fenced".main: sixrequiresliterals withtriggersand noautomation, all loader and publish-route token-list tests. NodefineStack, no flows, so none is affected.§5 The one-token sentence, restated at every site that repeated it:
validateTriggerCapabilityandStackTriggerCapabilityRequiredErrordocblocks;automation/flow-trigger-kind.ts;content/docs/automation/flows.mdxandcontent/docs/permissions/capabilities.mdx.@objectstack/lintcarries no trigger-capability rule of its own; it shares onlyresolveFlowTriggerKind. There is no asymmetry to report.Pins (table-driven: requires × kind × status)
packages/spec/src/stack-requires.test.ts, new block. Each refusal is asserted as its envelope (code,status: 422, oneissuesentry per flow) plus the prescription text:['triggers']refused forrecord_change,schedule,time_relativeandapi;['automation', 'triggers']accepted for all four, and in any order (the control);[]and absent: the pair named, andAdd requires: ['triggers']asserted absent;['automation']: today'sissuesentry asserted withtoEqual;screenflow, a hand-launchedautolaunchedflow) unaffected;obsolete/invalidunaffected, whiledraft/activeare refused;Ablation, committed first and restored by blob hash:
7 failed | 23 passed.3 failed | 27 passed.30 passed, blob4c0bfced0f02equal to HEAD.os gpin flipped (packages/cli/test/generate-stack-reach.test.ts, landed with PR #20329).os g flow order_lineinto a['triggers']project was pinned as "cannot run", exit 0. The written flow now stops the config from loading, so the write is refused. The pin is now: exit 1, the project tree byte-identical, the flow file absent, stdout namesdoes not include 'automation'and printsrequires: ['automation', 'triggers'], and noCreated. Result:7 passed.Tests and gates
The branch head is
94e32023d4: a merge oforigin/main6704717188made throughscripts/pm/os-regen-merge.sh. It touchederror-code-ledger.zod.tson both sides, and both edits survive. Every reading below was taken at that head unless it names45cfbaafee, the last commit before the merge. The merge brought no change to any file those older readings depend on.@objectstack/specat94e32023d4:vitest run --project local: 554 files, 16439 passed, 1 todo.typecheckexit 0.check:generated: every artifact up to date, after a rebuild.@objectstack/cliat45cfbaafee:generate-object-namespace-prefixandgenerate-scaffold-wiring(unit): 52 passed.generate-stack-reach(integration): 7 passed.@objectstack/lint, the consumer suite, at45cfbaafee: 1 failed | 4303 passed. The one failure is the fenced fixture described below. Its new message, re-read at94e32023d4, is the refusal it should be. This is expected, and owed.requires = ["automation","triggers"]). The boot pin itself is left to CI's Dogfood Regression Gate.os validateon the examples at45cfbaafee:app-todo,app-showcaseandapp-crmeach exit 0.dispatch-gates --commandsat94e32023d4derives 112. All 112 ran, each with its exit code recorded, and--ranreconciles them: 112 run, 0 NOT-MEASURED, 0 UNRUN.@objectstack/speccheck:*in the list (api-surface,authorable-surface,docs,error-code-provenance,liveness,skill-examples),check:type-check-debt,check:dual-build-cjs-loads, and the rootcheck:*andnode scripts/*set.check-empty-changeset. See the next section.Deliberate correction of a pending release note
.changeset/20215-generate-scaffolds-reach-stack.md(from PR #20329, unreleased) says two things this PR makes false:automation, the server loads the flow and never runs it."os greports cannot run for a flow in a stack whoserequireslacksautomation.Both sentences are corrected in place.
content/docs/deployment/cli.mdxgets the same correction.check-empty-changesetstays red on this, by design ("DELIBERATE CORRECTION … say so on the PR and get it confirmed"). This needs a person's confirmation. Restoring the file from base would put the false sentences back into the next release.Owed, and fenced (not in this PR)
These are outside the dispatch's fence, so this PR does not touch them:
packages/lint/src/authoring-rule-input-tier.test.ts:93:requires: ['triggers']becomes['automation', 'triggers'], and its comment names the pair. Until then@objectstack/lint's suite is red on that one test.packages/cli/src/commands/init.ts(the emitted config comment,:649–652, and theSCAFFOLD_WIRED_REQUIRESdocblock) andpackages/cli/src/commands/generate.ts(the emitted flow-file header,:366–369, and its docblock). Each says that withoutautomation"the server loads the flow and never runs it". After this change the config stops loading.Acceptance notes
cannot runbranch ofos g's reach report has no scaffold that reaches it now. The flow scaffold's only tokens are the pair, and a stack missing either is refused. This is dead for today's generators. It is noted and not filed. Carrier: the next PR to touchpackages/cli/src/commands/generate.ts.os validateon a config that exports a plain object instead of callingdefineStack()skips everydefineStackcross-field refusal, this one included. Measured: a plain-object probe withrequires: ['triggers']and arecord_changeflow exits 0 at this head. This is the whole refusal family's door, not this arm's, so it is reported to the seat in the dev report and not filed here.Generated by Claude Code