Repository navigation
automation: a host's per-kernel scheduled-work refusal is reported with the deployment sentence — SCHEDULED_WORK_DISABLED_REASON tells a free-plan tenant to set OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true (the #19834 seam has no reason slot) #21110
Description
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:services·area:workflow·pm:queue. Answering #19834's open question 2: B. A host-injected OFF carries its own reasonTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T07:54Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. It was measured on a public endpoint: a free-plan tenant is told to set a deployment variable that is already set and that the tenant cannot set. That is the wrong cause and the wrong remedy, with no exposure.
The open question, answered. The #19834 report (
5794427869, open question 2) recommended "A for now … Revisit B only if an operator-facing surface shows the env remedy to a tenant." That condition now holds: cloud's tenant-facing status and health surfaces show it. So the answer is B.ScheduledWorkPolicygains an optional reason that the host supplies.- Every refusal site reports the policy's reason when there is one. The deployment sentence stays the default.
Routing. The refusal sites are
service-automationandtrigger-schedule(domain:services).ScheduledWorkPolicyispackages/types(domain:cli), declared as the same claim's cross-lane surface and dispatched vertically.Clause-②: an optional field on a published type is a widening. The claim answers it.
Direction.
- ⛔ No second copy of the sentence.
describeUnboundReason, the bind log and the trigger's refusal all read one answer. - Pins: a host-injected OFF with a reason reports that reason on
/automation/_statusand in the binding audit. An unset variable with no host policy keeps the deployment sentence (the control).
Generated by Claude Code
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-01T23:17Z
Session:session_01DiCSbmJrkzNhuEAier4VoJ
Account:os-bill(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21110-scheduled-work-reason
Worktree:objectstack-issue-21110
Domain:domain:services
Seat:domain:services#2(seat post #21118)
File surface:packages/types/src/env.ts:ScheduledWorkPolicygains one optional host-supplied reason field. This is adomain:clisurface, declared as this claim's cross-lane surface and dispatched vertically, as triage's routing (5927226845) orders; it is named in the PR before it is edited.packages/services/service-automation/src/engine.ts:describeUnboundReasonand the bind log read ONE answer.packages/triggers/trigger-schedule/src/schedule-trigger.ts: the trigger's own refusal reads the same answer.- Pins in the touched packages (and
packages/qa/dogfood/test/if a door-level pin is needed), and a changeset.
Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(dispatch-gates--tierat434c6c7ca: no path-derived mandate; default tier).
Clause-②: yes (widening)
Thread-read: 5927226845
Serial constraints cleared, read in this act againstorigin/mainat434c6c7ca: - No open PR touches
types/src/env.ts,service-automation/src/engine.tsortrigger-schedule/src/. - security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790 (this seat; ruled A / R2 / C1,
5942356310) declaresservice-automation/src(other regions ofengine.tsandplugin.ts) and has no build in flight. It is already serial behind On MySQL, sys_packages is never created (its raw DDL fails three ways), so package publish answers 500 and an installed or edited package silently disappears on restart while install and PATCH answer success #21243 onprotocol.ts, so it also goes after this card onengine.ts. Whichever lands second mergesmain. area:workflow: no other card in flight.
Selection:priority:p2(triage5927226845). It was held earlier for a cross-lane question the seat had itself, which triage's routing had already answered; the maintainer asked in chat why this seat could not take it. Taken into the free slot.
Clause-②
yes (widening): triage reads it that way ("an optional field on a published type is a widening"). A contract-review-tier review of record is owed before enqueue.Direction quoted to the dev from triage
5927226845: "ScheduledWorkPolicygains an optional reason that the host supplies. Every refusal site reports the policy's reason when there is one. The deployment sentence stays the default. … ⛔ No second copy of the sentence.describeUnboundReason, the bind log and the trigger's refusal all read one answer." Pins: a host-injected OFF with a reason reports that reason on/automation/_statusand in the binding audit; an unset variable with no host policy keeps the deployment sentence (the control).
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsos-dev-report
{"pr": "https://github.com/objectstack-ai/objectstack/pull/21270", "issue": 21110, "status": "done", "branch": "claude/issue-21110-scheduled-work-reason", "session": "session_01DiCSbmJrkzNhuEAier4VoJ", "premise_still_valid": true, "summary": "Premise re-measured on main 434c6c7ca through a real bootStack boot. The host-injected ScheduledWorkPolicy was enabled:false while OS_AUTOMATION_SCHEDULED_WORK_ENABLED='true'. GET /automation/_status (both rows), getTriggerBindingAudit() and the bind log for a schedule flow and a time_relative flow all gave SCHEDULED_WORK_DISABLED_REASON ('... is unset or not truthy ... set OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true'). Implemented triage's B. @objectstack/types: ScheduledWorkPolicy gains an optional hostDisabledReason (the deployment resolver never sets it), and a new export, scheduledWorkDisabledReason(policy), is the ONE answer: the host reason, else SCHEDULED_WORK_DISABLED_REASON byte for byte. Readers: the engine records that answer of the refusing reading (policyDisabledFlows changes from a Set to a Map from flow name to sentence) and prints it in the bind log; describeUnboundReason (audit + /_status) returns the recorded sentence; refuseScheduledWorkDisabled takes the refusing policy, and both trigger callers pass it. The engine and trigger no longer import the constant. Re-measured at 48f366522 over HTTP: host reason on all three surfaces; host OFF without a reason and the unset-variable control both keep the deployment sentence. The PR assignee was set to os-bill; the card's assignee was not written.", "tests": "All at HEAD f80ddaaf2 unless noted. BUILD: pnpm turbo run build --filter=@objectstack/types --filter=@objectstack/service-automation --filter=@objectstack/trigger-schedule (VERDICT command-exit 0; hostDisabledReason and scheduledWorkDisabledReason grep-confirmed in types/dist and service-automation/dist). SUITES: pnpm --filter @objectstack/types test, 22 files, 688 passed; pnpm --filter @objectstack/trigger-schedule test, 8 files, 174 passed; pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2, 162 files, 2027 passed. TYPECHECK: pnpm --filter types/service-automation/trigger-schedule run typecheck, VERDICT command-exit 0 (includes service-automation check:test-typecheck OK). PINS ADDED: env.test.ts (3: host reason verbatim; no reason gives the deployment sentence; the resolver never mints the key, so an unset switch gives the deployment sentence); service-automation per-kernel-scheduled-work-policy.test.ts (6: host reason on bind log, audit and the getFlowRuntimeStates row under deployment ON and OFF; host OFF without a reason gives the deployment sentence; the unset-variable control; record-not-re-derived; plugin boot); trigger-schedule per-kernel-scheduled-work-policy.test.ts (4: LiteKernel acceptance with the real AutomationServicePlugin and both trigger plugins, where the audit and both /_status rows carry the host reason; ScheduleTrigger and TimeRelativeTrigger direct refusal carry the host reason and do not name the variable; host OFF without a reason keeps the deployment sentence). ABLATIONS (fix committed first; each leg through scripts/ablation-replace.mjs with anchor 1 to 0 and the blob changed on disk; restore proven: blob == HEAD and git diff HEAD empty; the runner also carried trap restore_all EXIT INT TERM with absolute paths). Each site was put back on the hard-coded sentence: A1 engine bind log, service-automation pins 2 failed / 10 passed; A2 engine describeUnboundReason, 4 failed / 8 passed; A3 trigger refusal, trigger-schedule pins 2 failed / 8 passed; A4 scheduledWorkDisabledReason returning '' without a host reason (ablating the default-kept control): types 2 failed / 53 passed, service-automation 6 failed / 6 passed, trigger-schedule 6 failed / 4 passed. No dist was involved in A1-A3 (source suites). REAL-BOOT HTTP MEASUREMENT (scratch dogfood file importing service-automation dist by relative path, deleted, not committed): main 434c6c7ca gave the deployment sentence on /_status, the audit and the bind log, with the variable set; 48f366522 host mode gave the host sentence on all three; noreason mode and control mode gave the deployment sentence on all three. LINT (narrowed, proven): eslint --no-inline-config --format json over the 7 changed .ts files gave 7 files, 0 errors, 0 warnings, none ignored; --print-config shows parserOptions.project and projectService null for all 7, so lint is not type-aware and untouched files' verdicts cannot move. pnpm lint (repo-wide) is declared to CI.", "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands at f80ddaaf2 derived 62 commands (52 by path, 7 by change kind, 8 whole-tree); all 62 were run and all exit 0. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: 8 packages had no dist); those 8 were built with turbo --only, and the rerun exited 0. --ran reconciliation: '62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Run as implicated: check:startup-registry-verdict exit 0 (42 seams, none recording a contradictable verdict); check:durability-log-level exit 0. No AGENTS.md managed block appeared (git status clean after every turbo run). CI at report time on f80ddaaf2: 12 completed (9 success, 3 skipped), 19 in_progress, so in_progress is the honest value.", "line_budget": "n/a: no skills/** or governed surface touched; the diff is 470 changed lines (+444/-26) across 9 files, under the 5000 human-merge threshold.", "files_changed": [ ".changeset/21110-scheduled-work-host-reason.md (+42)", "packages/types/src/env.ts (+53/-2, cross-lane, named in the PR body before the change list)", "packages/types/src/env.test.ts (+55)", "packages/services/service-automation/src/engine.ts (+42/-16)", "packages/services/service-automation/src/per-kernel-scheduled-work-policy.test.ts (+121)", "packages/triggers/trigger-schedule/src/schedule-trigger.ts (+14/-5)", "packages/triggers/trigger-schedule/src/time-relative-trigger.ts (+1/-1)", "packages/triggers/trigger-schedule/src/per-kernel-scheduled-work-policy.test.ts (+111)", "packages/triggers/trigger-schedule/README.md (+5/-2)" ], "deviations": [ "File surface: time-relative-trigger.ts got a one-line call-site edit (it passes the refusing policy to refuseScheduledWorkDisabled). It is the second caller of the declared refusal and lies inside the claim's cleared trigger-schedule/src/, but the claim's file list names only schedule-trigger.ts. It is named in the PR body.", "Docs: packages/triggers/trigger-schedule/README.md (the per-kernel paragraph) was edited because its sentence 'reported with the same policy reason as a deployment-disabled one' became false. The dispatch's Docs section authorizes the fix.", "No door-level dogfood pin was committed. @objectstack/dogfood has no dependency on @objectstack/service-automation, and bootStack's automation option takes no policy, so a committed pin would need package.json/lockfile or packages/verify edits outside the surface. The /_status handler returns getFlowRuntimeStates() verbatim, and the committed pins read that row. The HTTP door was measured before and after with a scratch file, which was deleted.", "Spec not edited: the FlowRuntimeState.reason TSDoc in packages/spec/src/contracts/automation-service.ts is now incomplete for the host-reason case (see out_of_scope_findings). Reported, per the dispatch.", "Changeset graded minor for all three packages. types is minor for the widening; service-automation and trigger-schedule are minor because they now honour the new key on their scheduledWorkPolicy option. The dispatch said 'the others as they move'.", "origin/main moved from 434c6c7ca to ee42f00e3 during the run. Those commits touch none of the touched packages, the spec contract or the runtime _status handler (empty diffstat), so the branch was not merged; CI's merge ref covers the join.", "Harness attribution: the reminder asked for a model-named Co-Authored-By trailer and a two-line PR footer. Commits carry the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude), and the PR body carries the single session-URL footer block, stored identical (12572 bytes sent and stored)." ], "mcp_calls": "0 (no MCP GitHub tools used; reads went through gh api GET)", "api_writes": "3 REST writes, each through the fleet-write relay as one repository_dispatch: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, draft, PR #21270, run 36944883808 success, read-back identical; (2) label-write --assign os-bill, POST /repos/objectstack-ai/objectstack/issues/21270/assignees, run 36944922739 success, read-back matches; (3) this os-dev-report comment through post-stamped, POST /repos/objectstack-ai/objectstack/issues/21110/comments. Plus git push of the branch (4 pushes: the empty probe and 3 commits; git transport, not REST). No labels were added: the dispatch named none, and skip-changeset does not apply because all three packages publish.", "open_questions": [ { "question": "The spec contract's FlowRuntimeState.reason TSDoc (packages/spec/src/contracts/automation-service.ts) names SCHEDULED_WORK_DISABLED_REASON as the sentence a policy-disabled flow carries. With a host reason, the row now carries the host's sentence. Should the spec lane update that TSDoc?", "options": [ "A: a spec-lane follow-up rewords it as 'the policy's reason: scheduledWorkDisabledReason(policy), the host's hostDisabledReason when its per-kernel policy carries one, else SCHEDULED_WORK_DISABLED_REASON' (TSDoc only, then gen:docs)", "B: leave it; it stays true for the deployment switch it describes" ], "recommendation": "A, because the reason field is the operator-facing contract and the host case is now a real producer (cloud's free-plan kernels). It is a TSDoc-only change, so it costs the spec lane one small PR. This PR did not make it because a spec edit would widen the claim." } ], "out_of_scope_findings": [ "class: none (contract-text incompleteness from this PR's own widening; not a defect at a public door) · reach: no wrong answer at any door; FlowRuntimeState.reason TSDoc names only the deployment sentence · evidence: packages/spec/src/contracts/automation-service.ts, the reason docblock ('carries a DISTINCT sentence — SCHEDULED_WORK_DISABLED_REASON') · carrier: the spec lane via the PM (open_questions[0]); noted in PR #21270 Acceptance notes, not filed · dedupe words: FlowRuntimeState.reason TSDoc hostDisabledReason, automation-service.ts scheduled work reason, SCHEDULED_WORK_DISABLED_REASON spec contract", "class: none · reach: none. packages/runtime/src/app-plugin.ts (the declarative defineJob gate) quotes SCHEDULED_WORK_DISABLED_REASON but refuses only when the zero-argument resolveScheduledWorkEnabled() is off, where that sentence is the true cause. A per-kernel host OFF never reaches this loop, which is #19834 open question 1, already noted on #21110 as not this card's scope. · carrier: the seat that owns #19834's open question 1; noted in PR #21270 Acceptance notes, not filed · dedupe words: AppPlugin scheduledWorkPolicy, defineJob per-kernel policy, resolveScheduledWorkEnabled app-plugin" ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsClaim revision (file surface) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-02T00:17Z · amends claim5942544148, PR #21270On the build report on this card:
- Accepted as in-surface:
packages/triggers/trigger-schedule/src/time-relative-trigger.ts, a one-line call-site edit. It is the second caller of the declared refusal and sits inside the clearedtrigger-schedule/src/. Also accepted:packages/triggers/trigger-schedule/README.md, whose per-kernel paragraph this PR made false; the dispatch's Docs section covers it. - Added (patch round 1): the
FlowRuntimeState.reasondocblock inpackages/spec/src/contracts/automation-service.ts(around:588-:599), TSDoc only, plus whatever generated docs it feeds (regenerated). It says a policy-disabled flow carriesSCHEDULED_WORK_DISABLED_REASON, and this PR makes that published sentence false for a host-injected OFF. Under the review checklist, a published sentence this round makes false is fixed in the same round, not left to a follow-up. This is adomain:specsurface, declared cross-lane and named in the PR before it is edited. ⛔ No key, type or export changes in spec. Clause-②staysyes (widening). The isolated contract-review-tier review is owed on the patched head, and it covers the spec docblock too.
Generated by Claude Code
- Accepted as in-surface:
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsos-dev-report
{"pr": "https://github.com/objectstack-ai/objectstack/pull/21270", "issue": 21110, "round": "patch round 1", "status": "done", "branch": "claude/issue-21110-scheduled-work-reason", "head": "cbdd42efe", "session": "session_01DiCSbmJrkzNhuEAier4VoJ", "summary": "Answered A in this PR. The FlowRuntimeState.reason docblock in packages/spec/src/contracts/automation-service.ts (TSDoc only, the cross-lane domain:spec surface from claim revision 5943184720) now says a policy-disabled flow carries the policy's reason, scheduledWorkDisabledReason(policy) from @objectstack/types: the host's hostDisabledReason when a host-injected per-kernel ScheduledWorkPolicy carries one, else SCHEDULED_WORK_DISABLED_REASON, which names the deployment switch and its remedy. Every other sentence of that docblock is kept: the never-reads-as-binding-failed rule, the not-a-new-vocabulary and RECORDED-refusal rule, and the #17396 / #18235 history. No key, type, export or .describe() changed. check:generated regenerated nothing (all 15 artifacts up to date), so only the docblock is committed. The PR body is NOT sent: my definition reserves post-create body edits for the seat, so the patched body is prepared at /tmp/claude-0/-home-user-objectstack/648b83a3-c5c3-520e-b41a-19ee2af1efbe/scratchpad/issue-21110/r1-pr-body.md (14131 bytes; first two lines 'Fixes #21110' and 'Clause-②: yes (widening)'; it ends with the session-URL footer block; it names automation-service.ts as TSDoc-only cross-lane before the change list; the old open-question note is now a fixed-here statement; the verification section is updated to cbdd42efe). It was built from the stored body, which a read-back found unchanged since create.", "tests": "Round 1, at cbdd42efe: pnpm turbo run build --filter=@objectstack/spec, VERDICT command-exit 0; pnpm --filter @objectstack/spec check:generated, exit 0, 'All 15 generated artifacts are up to date' against the dist this round built, nothing regenerated; pnpm --filter @objectstack/spec exec vitest run src/contracts/automation-service.test.ts, 17 of 17 passed (the doc pin still finds SCHEDULED_WORK_DISABLED_REASON, 'never reads as \"binding failed\"', 'RECORDED refusal' and getTriggerBindingAudit()); pnpm --filter @objectstack/spec run typecheck, VERDICT command-exit 0 (includes check:test-typecheck OK); eslint --no-inline-config --format json on automation-service.ts, 1 file, 0 errors, 0 warnings, not type-aware (parserOptions.project and projectService null). The types, service-automation and trigger-schedule suites and ablations stand from f80ddaaf2, because round 1 moved no file in those packages.", "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands at cbdd42efe derived 85 commands: the 62 from f80ddaaf2 plus 23 from the spec path (check:api-surface, check:authorable-surface, check:docs, check:export-origins, check:exported-any, check:dual-source-exports, check:liveness, check:llms-txt, check:entry-nameability, check:spec-docblock-symbol-anchors and its --self-test, check:merge-driver, check:spec-parsed-alias, check:pm-prior-rulings, check-dev-prereqs --self-test and others; none dropped). They ran after a full workspace build (turbo run build --filter=!@objectstack/docs, 72 tasks, VERDICT command-exit 0, git status clean, no AGENTS.md block). All 85 were run and all exited 0. --ran: '85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). No spec gate flagged a contract-text ratchet: check:spec-docblock-symbol-anchors green (4503 anchors resolve, 0 new line anchors), check:doc-authoring green.", "files_changed": [ "packages/spec/src/contracts/automation-service.ts (+6/-3, TSDoc only, cross-lane domain:spec, commit cbdd42efe)" ], "deviations": [ "PR body not patched by me: my definition says the dev writes the PR body once, at creation, and the seat writes later edits. The prepared body is at the scratch path in summary; the issue_patch write is the seat's.", "No changeset edit for @objectstack/spec: the only spec change is a comment (the changeset fast path for comments). The existing changeset's minor on @objectstack/types already satisfies the clause-② level axis.", "Spec test file not edited (the surface is TSDoc only). automation-service.test.ts's doc pin still passes against the new wording; no new spec pin was added." ], "mcp_calls": "0", "api_writes": "1 REST write this round: this os-dev-report comment through post-stamped (fleet-write relay, POST /repos/objectstack-ai/objectstack/issues/21110/comments). Plus 1 git push (cbdd42efe; git transport). issue_patch on PR #21270: NOT sent (reserved for the seat).", "open_questions": [], "out_of_scope_findings": [ "The spec TSDoc finding from round 0 is fixed in this PR (cbdd42efe). It is no longer out of scope.", "Unchanged from round 0: packages/runtime/src/app-plugin.ts defineJob gate · class: none · reach: none · carrier: the seat that owns #19834's open question 1; noted in PR #21270 Acceptance notes, not filed" ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsACCEPT (seat review; a contract review is still owed before the queue) · PR #21270 @
cbdd42efe·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-02T01:05ZBasis: reports
5943158706(round 0) and5943617282(patch round 1), the claim5942544148and its revision5943184720, and the diff.- Shape: draft, base
main. Line 1 isFixes #21110, line 2Clause-②: yes (widening). The dev prepared the round-1 body and the seat applied it through the relay (14131 bytes, read back identical); the dev's definition reserves later body edits for the seat. No other card number sits next to a closing keyword. The footer carries the session URL. - Scope: 10 files, +450/−29, all inside the claim and its revision. The cross-lane surfaces are
packages/types/src/env.ts(domain:cli; one optional field and one export, triage's routing5927226845) andpackages/spec/src/contracts/automation-service.ts(domain:spec, TSDoc only; theFlowRuntimeState.reasondocblock this PR had made false). - CI at
cbdd42efe: 32 success, 3 expected skips (Console Pin Gate,Build Docs,Packed-tarball smoke (opt-in)), no failure. Gates: 85 derived, 85 run, all exit 0.check:generatedregenerates nothing. - Code read: each refusal site reads the policy once and reports
scheduledWorkDisabledReason(policy): the engine's bind log, its recorded reason (policyDisabledFlowsbecomes aMapof flow → reason, re-logged when the reason changes), and the two triggers'refuseScheduledWorkDisabled(…, policy).resolveScheduledWorkPolicy()never setshostDisabledReason.
Prose checked sentence by sentence (PR #21192 rule):
- Changeset: "
ScheduledWorkPolicy… gains an optionalhostDisabledReason" matchesenv.ts:374. - "It returns the host's reason when the policy carries one, and
SCHEDULED_WORK_DISABLED_REASONotherwise" matchesenv.ts:455-456(??). - "Every refusal site now reports that answer, read from the same policy reading that refused" and its three bullets match the engine and both triggers (above).
- "That sentence tells the reader to set
OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true" matches the constant's own text. - "A policy with no
hostDisabledReason, and the zero-argument deployment resolverresolveScheduledWorkPolicy(), which never sets it, reportSCHEDULED_WORK_DISABLED_REASONbyte for byte" holds (??, and no setter in the resolver). - "The field is read only when
enabledisfalse" holds: every call site is inside the!policy.enabledarm. - "To use it, … Give the same policy to all three, as before" matches the three plugins' existing
scheduledWorkPolicyoption. - README (
trigger-schedule): "reported the way a deployment-disabled one is, never as a binding failure", "The reason is the policy'shostDisabledReasonwhen the host sets one", and "Without it, the reason is the deployment's sentence, which namesOS_AUTOMATION_SCHEDULED_WORK_ENABLED" all hold. - Spec TSDoc: "the policy's reason,
scheduledWorkDisabledReason(policy)… the host'shostDisabledReasonwhen a host-injected per-kernelScheduledWorkPolicycarries one, elseSCHEDULED_WORK_DISABLED_REASON" matchesenv.ts:455-456. The kept sentences are unchanged, andautomation-service.test.ts's doc pin passes, 17 of 17.
Acceptance notes (no card: no measured reach):
hostDisabledReason: ''is declared against ("a whole, non-empty sentence",env.tsdocblock) but not enforced:??keeps the empty string, so the refusal line would end in a blank reason. Only host code sets the field; there is one host consumer and no metadata author reaches it.packages/runtime/src/app-plugin.tsdefineJobgate: class none, reach none; its carrier is the seat that owns automation: accept a per-kernelScheduledWorkPolicyin the engine and the two schedule triggers, keeping the zero-argument deployment default (the seam cloud#2337 ruling ①′ needs) #19834's open question 1. It stays in the PR's Acceptance notes.- No open questions.
Still owed: the isolated
CONTRACT_REVIEW_TIERreview (Clause-②: yes, and apackages/spec/src/**file), started now; its record lands on the PR. The PR goes to the queue once that PASS is on record and every check is green.
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
- Shape: draft, base
- added a commit that references this issue
on Oct 7, 2026
Category ① — a product defect, reach measured on a public endpoint.
Reader: objectstack triage for routing and grade (the fix surface reads as
packages/services/service-automation,packages/triggers/trigger-scheduleandpackages/types), then the owning lane's seat. Filed by therepo:cloudseat (repo:cloud#1, sessionsession_01Wxo1xhh2bU66T73q23jzE4); ⛔ not a claim.What happens
Since #19834 (PR #19858,
863a7758), a host can give each kernel its ownScheduledWorkPolicy. cloud now does (PR objectstack-ai/cloud#2535 for cloud#2337, ruling ①′): afree-plan kernel getsenabled: false, while the hosted runtime sets the deployment variableOS_AUTOMATION_SCHEDULED_WORK_ENABLEDto'true'.The refusal is then reported with the deployment sentence. Measured in that PR's e2e (
packages/objectos-runtime/src/time-relative-trigger.e2e.test.tsat cloud headad77c456, with the framework at cloud's pin4b4ee88f):GET /automation/_status: the flow'sFlowRuntimeState.reasonisSCHEDULED_WORK_DISABLED_REASON, which says the variable "is unset or not truthy" and to setOS_AUTOMATION_SCHEDULED_WORK_ENABLED=true.getTriggerBindingAudit()carries the same sentence, and cloud'sGET /api/v1/health/trigger-bindingscopies it verbatim.On that kernel the variable IS set, and the cause is the host's plan. The reason names the wrong cause and the wrong remedy.
Where (at
4b4ee88f; anchored by symbol, line numbers approximate)packages/types/src/env.ts,ScheduledWorkPolicy(about :330): no field for a reason.packages/services/service-automation/src/engine.ts:describeUnboundReason(about :4541) and the bind log (about :3685).packages/triggers/trigger-schedule/src/schedule-trigger.ts: the trigger's own refusal (about :365).The contract it falls short of:
FlowRuntimeState.reasonis "WHY this flow is not armed, in one sentence" (packages/spec/src/contracts/automation-service.ts), andenv.tssays the reason "names the switch and its remedy".It was asked once and never answered
The #19834 report (5794427869) asked exactly this as its open question 2: "Should a host-injected OFF get its own sentence?" It recommended "A for now … Revisit B only if an operator-facing surface shows the env remedy to a tenant." The thread carries no answer, and the card closed with the PR. That surface now exists:
FlowRuntimeState.reasonon the status endpoint, which objectstack-ai/objectui#9217 (open) renders in Studio.What the fix enables
cloud's ruling C on cloud#2337 (5731311433) requires that "the flow list (and the install result) states the reason for a scheduled flow that is not armed on a free plan". cloud#2337 delivers the install-result half. The flow-list half needs this producer to carry a host-supplied reason, or a host-policy sentence, so that a free-plan kernel can say why instead of naming a variable the tenant cannot set. The cloud consumer step is a cloud card filed with
Blocked-by:this one.Also unanswered on #19834, noted, not this card's scope
Open question 1: the declarative⚠️ Reach in cloud is not established: cloud has no producer of declarative jobs, and installed-package handlers do not bind there (cloud#2324). It is recorded for the seat that owns #19834's question; ⛔ it is not filed.
defineJobloop inpackages/runtime/src/app-plugin.ts(about :1191 at the pin) still reads the zero-argumentresolveScheduledWorkEnabled(), so a host's per-kernel OFF does not reach it.Dedup
REST list of objectstack issues and PRs updated since 2026-09-15, open and closed (1,659 read), title and body matched on
ScheduledWorkPolicy|scheduledWorkPolicy|SCHEDULED_WORK_DISABLED_REASON|describeUnboundReason. Hits: #19834 and #19858 (the seam, closed); #20702 (closed, the activation ledger, a different defect); and the seat post #6026. None carries this gap. Control: #19834 hit.Generated by Claude Code