Skip to content

fix(triggers,spec,service-automation): a time-triggered flow declares its acting organization and runs as it (#16659) - #17126

Draft
os-trump wants to merge 14 commits into
mainfrom
claude/issue-16659-schedule-trigger-acting-organization
Draft

fix(triggers,spec,service-automation): a time-triggered flow declares its acting organization and runs as it (#16659)#17126
os-trump wants to merge 14 commits into
mainfrom
claude/issue-16659-schedule-trigger-acting-organization

Conversation

@os-trump

@os-trump os-trump commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16659

Ruling executed: 方向一, narrowed (director seat, comment 5577119379). Maintainer, verbatim:

多组织定时任务本来只能在组织内运行,应该带组织ID,不允许跨组织的定时任务。

Patch round. This body describes the head after the CONTRACT_REVIEW_TIER verdict CHANGES REQUIRED (F1–F9). The section "The patch round, finding by finding" below is the answer to that review; every other section has been brought up to date with it. Clause-② stays yes and needs:contract-review stays on both carriers.

The defect

A time-triggered flow launches its run from a job tick, and a job tick carries no identity, so ScheduleTrigger (and TimeRelativeTrigger) built an AutomationContext with no tenantId. Two consumers already read that key and both resolved NULL:

Both refusals happen one layer below anything that summarises the run, so the tick reported unmeasured=0 and read healthy.

Where the organization id lives, and why

A start-node config.organization key, declared in packages/spec.

  • Not flow-level. FlowSchema already refuses a top-level schedule in as many words — "a schedule flow declares its cron/interval as config.schedule on the START node, not at the flow top level". Every other trigger-binding fact lives on the start node, and resolveTriggerBinding hands the whole start config to the trigger. A flow-level key would split one binding across two layers.
  • Not inside the schedule descriptor. A time_relative sweep carries its cadence in that same slot but binds through a different descriptor, so a key inside schedule would be invisible to exactly one of the two triggers that owes it.
  • Not inferred from the flow's package. A package is installed platform-wide and carries no organization; inference would have to guess, which is the failure mode being closed.

Enforcement is at bind, not at FlowSchema.parse: the start node's config is an open record (ADR-0018), and a parse-time requirement would make every package-shipped scheduled flow in this repo unparseable. Bind-time refusal loses nothing — there is no path by which an organization-less time-triggered run reaches the data layer once bind refuses.

What landed

  • packages/spec/src/automation/schedule-organization.zod.tsSCHEDULE_ORGANIZATION_KEY, ScheduleOrganizationSchema, the ScheduleOrganization alias, resolveScheduleOrganization, findScheduleOrganizationNearMissInConfig, describeMissingScheduleOrganization. One refusal sentence and one near-miss scan, so the engine's lift and both triggers cannot drift.
  • AutomationEngine.resolveTriggerBinding lifts organization onto the schedule and time_relative bindings, beside schedule. record_change and api bindings leave it undefined by construction — both are fired by a caller who already carries an organization, and lifting a declared one onto them would let a flow overrule the tenant of the write that triggered it.
  • Both triggers refuse to bind an organization-less flow: stop() any prior binding first (a hot re-publish that REMOVES the key must not leave the previous job armed), log the reason at error naming the flow, then throw — see F1 below. The refusal names the near-miss spelling when the author wrote one (organizationId, tenantId, …); the start node's config is an open record, so those spellings are accepted and then ignored, and the refusal is the only place that becomes visible.
  • Both triggers set tenantId on the run context unconditionally. ⛔ Not ...(organization ? {…} : {}) — the bind refused the flow otherwise, and a conditional spelling would re-open the org-less run as a silent state.

⛔ No fan-out. ⛔ No fallback limb — not the install's only organization, not the platform organization, not the first row of sys_organization. A wrong organization_id is worse than a refusal: a refusal is visible at boot and names its flow, a wrong value is silently authoritative to every report, export and cleanup script that filters by organization.


The patch round, finding by finding

# verdict what changed
F1 fixed the refusal is thrown, and the extended pin proves the structured surfaces see it
F2 fixed the published trigger-schedule README declares the key in both examples and states the refusal
F3 fixed flows.mdx gains a dedicated section, both worked examples declare the key, hooks.mdx and capabilities.mdx point at it
F4 not settled here — #17150 named, and the showcase docstring stops promising it fires
F5 fixed the trigger calls the spec helper; the three consumer-less exports are gone; both false comments corrected
F6 fixed the promised refusal suite exists (schedule + time-relative), plus a spec unit test
F7 fixed the memory limb states the control is unavailable and pins the reason, at the right polarity
F8 fixed renamed to schedule-organization.zod.ts; own reference page; two further real gate findings fell out of it
F9 fixed the measurement is below; the lockfile churn is dropped

F1 (blocking) — the refusal now reaches the engine

FlowTrigger.start() returns void, so logging the refusal and returning was indistinguishable — to the engine — from arming successfully. activateFlowTrigger ran boundFlowTriggers.set(flowName, …) and logged Flow '…' bound to trigger 'schedule' one line after the trigger said NOT BOUND, and all three machine-readable channels then said the opposite of the stderr line.

refuseMissingOrganization (was reportMissingOrganization) now returns never: it logs the sentence at error, then throws the same sentence. That is the engine's designed catch path — the set is inside the try, after the call, so a throw skips it, and getTriggerBindingAudit() lists the flow with binding failed — see earlier warnings, which points at the error line already emitted. Loud channel and structured channel carry identical text and cannot drift.

Pin (3) is extended, as the review requires, with a new case (3, structured) the refused flow reads as NOT BOUND on every machine-readable surface:

  • getFlowRuntimeStates() reports bound: false for the refused flow — and bound: true for the declaring one, as the paired control;
  • getTriggerBindingAudit() contains the refused flow with triggerType: 'schedule' and a reason containing binding failed — and does not contain the declaring one.

The old pin could not have caught this: job.has(...) === false is a statement about the job service, which was never asked either way.

Ablation — the throw removed (swallowed at the schedule call site, so the limb reverts to log-stop()-return, i.e. the reviewed head's exact behaviour):

ANCHOR occurrences before mutation: 1
injected marker count: 1
deleted-shape count (bare refuse call as last stmt): 0
mutated blob: c68fa48368e5ee0c997f23edd5c5619c3a265aaf   HEAD blob: cc127f299813d0451d01c11ef06b7b30eb8f81b5
...
 FAIL  test/schedule-acting-organization.dogfood.test.ts > dogfood [sqlite-wasm]: … > (3, structured) …
 FAIL  test/schedule-acting-organization.dogfood.test.ts > dogfood [memory]:      … > (3, structured) …
AssertionError: Studio's status badge says this flow is armed while the trigger refused it …:
  expected true to be false
 Test Files  1 failed (1)
      Tests  2 failed | 14 passed (16)
RESTORED: blob cc127f299813d0451d01c11ef06b7b30eb8f81b5 == HEAD cc127f299813d0451d01c11ef06b7b30eb8f81b5, git diff HEAD empty

Exactly the two new cases went red, on both drivers, while everything else — including the old job.has(...) === false assertion — stayed green. That contrast is the review's point, reproduced. Restoration proved by blob equality and an empty git diff HEAD, under a trap … EXIT INT TERM with an absolute path; the mutation was proved to reach disk by two anchor greps (1 / 0) plus blob inequality before the run. ⚠️ No dist pre-flight leg here and that is not an omission: packages/qa/dogfood/vitest.config.ts aliases @objectstack/trigger-schedule to source (its own registered entry, landed earlier in this PR), so the suite reads the mutated file directly and there is no artifact to fall behind.

F2 (blocking) — the published README

packages/triggers/trigger-schedule/README.md ships in files[], and both of its worked examples were made false by this PR. Both now carry organization, the context line names tenantId, and a new section "The acting organization is required" states the refusal, where it surfaces (getTriggerBindingAudit(), the CLI startup summary, getFlowRuntimeStates()), that there is no fallback and no fan-out, and that a near-miss spelling parses and is then ignored. The timeRelative example carries the key too, with the stronger reason it owes one (it queries isSystem on purpose, so org-less it selects across every tenant).

F3 — hand-written docs

  • content/docs/automation/flows.mdx — both worked schedule examples declare the key, and a new "The acting organization" section carries the contract: what the run executes as, the bind-time refusal and the three surfaces that report it, no fallback, no fan-out, and a warning callout about near-miss spellings.
  • content/docs/automation/hooks.mdx — the "reach for a Flow" row now links to that section.
  • content/docs/permissions/capabilities.mdx — the requires: ['triggers'] row now says triggers is a capability, not the whole declaration.

⛔ Nothing under content/docs/releases/** was touched.

F4 — NOT settled here: #17150

The four package-shipped example flows (showcase_scheduled_digest, showcase_task_due_reminder, and examples/app-todo's task_reminder and overdue_escalation) are refused at bind under this change, and there is no legal value an author could write for a flow that ships inside a package: organization ids are minted at runtime, per install.

That question is #17150 (needs-user-decision, options A/B/C with a director reading recommending A′), and it is named here so it is no longer implicit. This PR changes no example flow's behaviour, invents no symbolic organization and writes no placeholder id — a value matching no row is exactly the "wrong organization_id, silently authoritative" outcome the ruling forbids, and strictly worse than the refusal.

What this PR does do is stop one of them lying: showcase_scheduled_digest's docstring no longer promises it is "observable end-to-end", and says instead that it does not fire as shipped, why, that a placeholder must not be invented, and that #17150 owns the answer.

⚠️ A related dependency this round measured, recorded not folded in. The manual platform checklist depends on these flows firing: docs/qa/platform-checklist/areas/automation.json expects "two ticks ⇒ two showcase_scheduled_digest run rows", expects the boot banner to count showcase_task_due_reminder as bound, and reads GET /api/v1/automation/showcase_task_due_reminder/runs; areas/platform-core.json uses the same digest as its notification-flood source. Those items go red under this change until #17150 is settled. docs/adr/0057's pilot narrative (that digest growing dev.db to 260 MB) likewise stops being reproducible. ⛔ Nothing in docs/ or .claude/ was edited — governed surface, reported not written.

F5 — the spec module's exports and the two false comments

  • reportMissingOrganization's inline near-miss scan is gone; it calls findScheduleOrganizationNearMissInConfig from packages/spec.
  • That helper takes the start node's config record, not a flow, and the name says so. A trigger never holds the flow — the engine parses the start node and hands it a binding whose config is that record — so a flow-shaped signature would answer undefined for the only caller's only input: a silent wrong answer, the exact class this module exists to end. The old findScheduleOrganizationNearMiss(flow) is therefore replaced rather than merely wired.
  • TIME_TRIGGERED_FLOW_KINDS and requiresScheduleOrganization are dropped (zero consumers; their would-be consumer is the lint rule, which cannot learn this requirement until [needs decision] #16659's ruled fix refuses the repo's own four example scheduled flows at bind, and today a package-shipped flow has no legal organization to name #17150 is settled), and SCHEDULE_ORGANIZATION_NEAR_MISSES becomes module-local (its only reader is the helper above). The repo's own rule, applied: an export whose consumers all live inside its own package does not belong on a published barrel.
  • Both comments that said FlowSchema / "validation" emits the refusal sentence are corrected — in automation/index.ts and on describeMissingScheduleOrganization — and both now state why enforcement is at bind instead.

F6 — the refusal suite the fixture comment promised

schedule-trigger.test.ts gains ScheduleTrigger — the acting-organization refusal (#16659) (7 cases) and resolveBindingOrganization (#16659) (8 cases): the throw, the error line carrying the same sentence as the thrown text, near-miss naming, that the refusal never echoes the author's value back, the "hot re-publish removes the key" stop() limb, per-flow isolation, the no-error-channel warn fallback, and every limb of resolveBindingOrganization (lifted field, raw-config fallback for an older engine, precedence, and the empty-string / number / object / null / absent cases). time-relative-trigger.test.ts gains the two load-bearing ones. The fixture comment now points at the suite by name.

packages/spec/src/automation/schedule-organization.test.ts is new — 28 cases over the key, the schema, resolveScheduleOrganization (including "not on the START node" and the structural non-throwing contract), the near-miss helper, and the refusal sentence, one of which asserts the sentence offers no fallback and names no organization.

F7 — control B on the memory driver

The old limb asserted status < 300 under a message claiming it pinned a 503: opposite polarity, no delivery check, certified nothing. The reason it certified nothing is structural — control B's whole discriminating power is the session being bound to an organization of its own, and this suite boots memory with orgContext: false precisely because driver-memory refuses any tenant-scoped call. A session with no organization is the unfixed state; a run triggered that way discriminates nothing.

So the limb now says that plainly and pins the reason, at the seam that makes it so: a tenant-scoped read on this driver produces no answer, and the refusal carries MULTI_TENANT_UNSUPPORTED. The row count is taken as a nullable value rather than with .toHaveLength, because .not.toHaveLength passes over a null target for the wrong reason. The day the driver gains isolation this goes red and whoever fixes it enables the real control. Control A (both drivers) and control B on sqlite still carry the proof.

F8 — .zod.ts, and the two gate findings that fell out of it

schedule-organization.tsschedule-organization.zod.ts. Regenerated: the module now ships via files[]: src/**/*.zod.ts and has its own reference page (content/docs/references/automation/schedule-organization.mdx); the Misc … (no single source file) bucket is deleted from references/index.mdx, automation/index.mdx and automation/meta.json.

⭐ The rename brought the module into scope of gates that read *.zod.ts only, and two of them were red — both real, both green now:

  • check:spec-parsed-aliasScheduleOrganization was z.infer, where ADR-0122 reserves the bare name for the AUTHOR state. Now z.input, with an isomorphism pin in type-alias-convention.pin.test.ts (Iso871, module slot M186) rather than a permanent ScheduleOrganizationParsed synonym: a bare z.string().min(1) has no default, transform or coercion. The pin file's count moves 815 → 816, in all three places it is stated.
  • check:llms-txt — the hand-kept inventory counts *.zod.ts modules, so automation and the total were each one short (13 → 14, 205 → 206), with the row's key-schema list updated rather than the number nudged.

Both were invisible while the file was a plain .ts. That is the substance of F8, beyond the docs page.

F9 — the dispatch-required measurement, and the lockfile

Question: does any deployment or test depend on scheduled runs not landing in sys_automation_run?

Measured answer: no. Method and evidence:

  1. Census. 108 files reference sys_automation_run; 38 are tests. Scanning every one of them for an absence or zero-count shape around the reference (toHaveLength(0), toBe(0), toEqual([]), toBeUndefined, toBeNull, not.toContain, "never"/"no run"/"zero" in prose) turns up no assertion that a time-triggered flow contributes no run-history row. The hits are about the object not being registered or readable at startup, about field shapes, or about the paused-visibility contract.
  2. Cross-reference. Of every suite that drives a schedule or time-relative binding (flow-schedule: / flow-time-relative: / ScheduleTrigger / TimeRelativeTrigger), exactly one also reads sys_automation_run: this PR's own pin.
  3. Deployment side. No retention override, quota, migration or config keys off run-history emptiness. sys_automation_run already declares two-sided retention — a per-flow cap on terminal rows at WRITE time (runHistoryMaxPerFlow, default 100) and declarative age retention (retention: { maxAge: '30d', onlyWhen: { status: { $in: ['completed','failed'] } } }, ADR-0057 / ADR-0057 data lifecycle follow-ups: retire per-plugin sweepers, dev telemetry datasource + db:clean, Studio surface, PG rotation (tracking) #2834, paused rows retained regardless of age). A minute-cadence flow is bounded by the per-flow cap, not by its tick rate.

Positive controls, because a zero from a probe that cannot answer "yes" is NOT MEASURED. The same method, unchanged, did find:

  • a zero-row assertion over run history where one exists — paused-run-visibility.test.ts (GET /automation/:name/runs → 200, zero rows); and
  • a real dependency in the opposite direction — docs/qa/platform-checklist/areas/automation.json ("two ticks ⇒ two showcase_scheduled_digest run rows"), reported under F4 above.

So the sweep can answer "yes", and here it answers "no".

Lockfile: the esbuild@0.28.10.28.2 swap in the vitest resolution strings of packages/apps/setup / studio is reverted. The diff against the merge-base is now exactly the three lines the new dev-dependency needs, and pnpm install --frozen-lockfile exits 0 leaving the file byte-identical (diff -q → unchanged).

⛔ One correction to this body's own earlier measurement

The previous revision said "no platform-organization constant exists anywhere in packages/ (zero non-test hits)". That is false, as the PM seat measured: packages/runtime/src/http-dispatcher.ts carries PLATFORM_ORG_ID (and SYSTEM_ENVIRONMENT_ID), used at :1412. The claim is withdrawn. The conclusion it supported survives on its own terms — that constant is private, unexported, not authorable, and a dispatcher sentinel is not a seeded sys_organization row — but the option analysis in #17150 starts from something rather than from nothing, and that is where it matters.


Verification

All figures below are from the tree at b998f58f3 unless a run is named otherwise.

The pin, and that it can fail

packages/qa/dogfood/test/schedule-acting-organization.dogfood.test.ts — real automation + messaging + ObjectQL stack, both drivers, under the exact condition the card measured: two sys_organization rows under the default single posture, which is the ambiguous-organization state system-write-organization.ts refuses on.

leg result
patched tree Test Files 1 passed (1) · Tests 16 passed (16)
ablated (throw removed) `Tests 2 failed

The earlier round's ablation of the whole fix (context tenantId removed) reddened 8 of 14 and is unchanged by this patch; the new one isolates F1 specifically, which is what the review asked to be provable.

Predictions, written before the runs

  1. Delivery — unfixed: sys_notification lands with organization_id = NULL and sys_inbox_message is EMPTY. Fixed: both carry the declared organization. Observed, both drivers.
  2. Run history — its own pin. Unfixed: no sys_automation_run row for the flow at all. Fixed: the row exists, carries the declared organization, and still names trigger_type = 'schedule' (trigger.recordId is never populated on record_change runs, and the persisted sys_automation_run row carries no trigger block at all — runs cannot be correlated to their triggering record, and trigger kinds are lost across restart #7533). Observed.
  3. The declaration error — unfixed: the organization-less flow binds like any other and ticks. Fixed: no job is registered for it, the refusal is error, names the flow, names the key, says NOT BOUND, mentions neither organization on the install — and the flow reads as not bound on getFlowRuntimeStates() and is named by getTriggerBindingAudit(). Observed, and the last clause is the one the ablation reddens.

Suites

suite result
@objectstack/spec test `Test Files 469 passed
@objectstack/service-automation test Test Files 127 passed (127) · Tests 1496 passed (1496)
@objectstack/trigger-schedule test Test Files 5 passed (5) · Tests 99 passed (99) (was 82)
@objectstack/dogfood (this pin) Tests 16 passed (16) (was 14)
typecheck (spec, service-automation, trigger-schedule) exit 0
pnpm lint (eslint . --no-inline-config, repo-wide) exit 0

⛔ No test was skipped, disabled or quarantined. The one skipped in packages/spec is pre-existing and untouched by this branch.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 113 families for this diff (112 last round, +1 check:examples-live-imports, from the showcase docstring edit). All 113 were run and reconciled:

Run reconciliation — 113 derived, 113 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 113 derived famil(ies) accounted for — 113 run, 0 NOT-MEASURED.

Five were red or unmeasured on the first pass; every one was resolved, none reported as a pass:

gate first pass resolution
check:spec-parsed-alias exit 1 — real finding ADR-0122 alias state fixed + isomorphism pin (F8)
check:llms-txt exit 1 — real finding inventory counts corrected (F8)
check:skill-examples exit 1 — PREREQUISITE (client-react unbuilt) pnpm build, then green: ✅ 258 prose examples type-check
check:dual-build-cjs-loads exit 3 — PREREQUISITE (8 packages with no dist/) pnpm build, then exit 0
check:type-check-debt exit 124 — timed out under a 300 s cap at 4096 MB re-run at 8192 MB: OK — 5 ledger entr(ies) re-measured … none above its recorded number (the gate pins its own 6144 MB tsc ceiling, #17151)

⚠️ Declared: dispatch-gates reports this tree as at least 16 commits behind origin/main, with 6 gate-relevant scripts changed across that range. The derivation is therefore about this tree; CI on the merge is the authority for anything those six moved.

Bump

minor on all three packages, with a BREAKING banner and an ADR-0087 disposition (not-required (no-migration-prescription)) — the launch-window lockstep convention, the same shape #16645 landed under. check-adr-0087-registration and check-changeset-no-major both green. Not major: the remedy is a sys_organization.id only the deployment holds, so objectstack migrate meta cannot mechanically supply it and inventing one is what the ruling forbids.

Clause-② carrier: still yes

packages/spec/src/automation/index.ts re-exports the module and @objectstack/spec/automation is a published entry; gen:api-surface, export-origins and declaration-map record the delta. The patch round narrows that delta — three exports dropped, one renamed — but does not empty it, and the new optional organization key on both published FlowTriggerBinding types is untouched. Carrier stands; needs:contract-review stays on both carriers.

Out of scope, deliberately

维护者速读(草稿)

改了什么 — 定时流程(schedule)和时间相对扫描(time_relative)现在必须在 start 节点上声明 config.organization,声明的组织成为该次运行的执行组织。没有声明的流程在启动时被拒绝绑定:以 error 点名该流程,并且引擎把它记成"未绑定"—— Studio 的状态徽章显示 bound: false,启动摘要的绑定审计会列出它。本轮补丁修的正是这后半句:上一版只打了日志,引擎仍然把它记成"已绑定"。

为什么改 — 定时任务由时钟触发,没有会话可以继承租户。多组织安装上,它下面所有租户作用域的写入(通知收件箱、sys_automation_run 运行历史)都被 #8844 的守卫拒绝,而这一层之下没有任何东西汇总这些拒绝 —— 于是这一次 tick 报告自己健康,实际什么都没送达。维护者的裁决是:定时任务本来就只能在组织内运行,应该带组织 ID。

风险与代价(含回滚) — 这是一次接受集收窄:今天能启动的、没有声明组织的定时流程,之后不再绑定。这是裁决要求的行为,不是副作用。回滚 = revert 本 PR,没有数据迁移、没有存储形状变化、没有已写入的行需要改写。

席位意见 — (待 at-tier 复核席位填写;实现席位低于 CONTRACT_REVIEW_TIER。)

你要做的 — 一件事,在 #17150 上:本仓库自带的四个示例定时流程(showcase 两个、app-todo 两个)按本 PR 的规则会在启动时被拒绝,而包内置的流程没有任何合法的组织 id 可写。⛔ 本 PR 不发明占位 id,也不改这四个流程的行为。相关连带影响已量到:手工平台测试清单里依赖这两个 showcase 流程真的触发的条目会转红,直到 #17150 有结论。


Generated by Claude Code


Generated by Claude Code

… its acting organization and runs as it

A `type: 'schedule'` flow and a `time_relative` sweep launch their runs from a
job tick, which carries no identity, so `AutomationContext.tenantId` was never
set. On an install holding more than one `sys_organization` every tenant-scoped
write beneath the run was then refused by the #8844 guard — the inbox rows a
`notify` node emits and the `sys_automation_run` history row — while the tick
still summarised itself healthy.

- `@objectstack/spec` declares the start-node `config.organization` key, its
  value schema, the two kinds that owe it, the near-miss spellings an author
  reaches for, and the one refusal sentence every enforcement point says.
- The engine lifts the declaration onto the schedule / time_relative binding.
- Both triggers REFUSE to bind a flow that declares none, naming it at `error`,
  and thread the declared organization onto the run as `tenantId`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/dogfood, @objectstack/service-automation, @objectstack/spec, @objectstack/trigger-schedule, touching 20 documentable anchor(s). ⚠️ 10 changed file(s) yielded no anchor (packages/qa/dogfood/package.json, packages/qa/dogfood/tsconfig.json, packages/qa/dogfood/vitest.config.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 10 changed file(s) yielded no anchor (packages/qa/dogfood/package.json, packages/qa/dogfood/tsconfig.json, packages/qa/dogfood/vitest.config.ts, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: organization_id (literal, 31 pages)
  • 3 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.

Coarse fallback — 135 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 fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7a6978838c6982e685a84f4f8729025e439e5d32 — the merge of head b998f58f3832d3f8f952172bbd9b28f668f18ed9 into base fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3, 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 7a6978838c6982e685a84f4f8729025e439e5d32 && git checkout 7a6978838c6982e685a84f4f8729025e439e5d32
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3 b998f58f3832d3f8f952172bbd9b28f668f18ed9 && git checkout -B drift-repro fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3 && git merge --no-ff b998f58f3832d3f8f952172bbd9b28f668f18ed9

node scripts/docs-audit/affected-docs.mjs --json fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3

⚠️ 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 fd5cff209f4416a5d8bd9b08eaa8d42a0bee06d3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file tests and removed size/m labels Sep 9, 2026
… differential controls

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tooling and removed size/l labels Sep 9, 2026
…reference total

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…gfood pin

The pin's subject is the trigger's own run-context construction, so a dist
merely behind would run it green against the old one — the exact shape this
card is about. check:test-source-alias and check:type-source-resolution both
name this as the required spelling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIERVerdict: CHANGES REQUIRED (audit reading; director seat, summon #18 segment 5, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-09T12:4xZ)

PR #17126 · head 7dc451e65474c330e6466c949a0b1da715bd4e77 (re-read at posting 12:45:46Z; unchanged, PR body edited 12:43Z) · reviewed 12:34Z–12:42Z.

  • Reviewed-by: isolated claude-fable-5-1 subagent, transcript-verified (77 harness model stamps, all claude-fable-5-1, zero residue; positive control 68 assistant / 50 user role tokens), adopted verbatim below. Fed only the card A schedule-triggered flow's notify delivers nothing on a multi-organization install: the run carries no organization, so the tenant-scoped inbox/delivery writes are refused (#8844) while the run reads healthy #16659 and its ruling, this PR, the CI check-runs, and the checked-out tree.
  • Implemented-by: the domain:services seat's dev session_012zTkyNHJ7TkuN2oXtP5x37 (mode:subagent), branch claude/issue-16659-schedule-trigger-acting-organization (newest Claim: on the card, 5600851172). Distinct sessions ⇒ not a self-review.
  • Reading for the seat: the ruling's direction is executed and Clause-② yes matches, but two blockers: F1 the bind-time refusal never reaches the engine — a refused schedule flow is recorded bound: true, drops out of the silent-miss audit, and the CLI startup summary says it is wired (fix: throw from start(), the engine's designed catch path); F2 the published trigger-schedule README's two examples are refused after this PR and were not updated. F4 (the four package-shipped example flows are now permanently refused at boot) is a maintainer decision or a follow-up card named in the body, not something to leave implicit. Patch round to the same dev; re-review owed on the patched head. ⛔ This seat cleared no carrier and touched no PR state.

  • Verdict: CHANGES REQUIRED
  • Head reviewed: 7dc451e65474c330e6466c949a0b1da715bd4e77 (branch claude/issue-16659-schedule-trigger-acting-organization, draft, base main; head had not moved at the end of review; merge-base 50b6f17d)
  • Clause-② reading: yes — the diff adds eight named exports to the published @objectstack/spec/automation barrel (api-surface/automation.json +8 rows, declaration-map +2, json-schema.manifest +1), a new optional key organization on the published FlowTriggerBinding type of @objectstack/trigger-schedule (schedule-trigger.ts:41) and of service-automation (engine.ts:446), plus a bind-time accept-set narrowing. Claim on the card (Clause-②: yes) matches. node scripts/pm/check-clause2-carriers.mjs --pair 17126exit 0 (both carriers agree, fixed spelling). check-widening-tells --declaration yes → 0; under --declaration no it would flag 21 tells (T2/T3), confirming yes is the only honest reading.
  • Governed surface / protocol label / dependencies: governed surface none (no docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md). packages/spec subtree touched is src/automation/** only — no protocol:data|ui|system|ai label is owed and none is on the PR (correct). dependencies label: (a) packages/qa/dogfood/package.json adds @objectstack/trigger-schedule: workspace:* (private package, private: true, needed by the new pin) and alphabetically moves the existing @objectstack/formula line; (b) pnpm-lock.yaml adds the matching link: entry, and swaps esbuild@0.28.10.28.2 in the vitest resolution strings of packages/apps/setup / packages/apps/studio — unrelated churn (main's lockfile has not moved since merge-base). New runtime dependency: no.
  • CI on head: 33 checks, 0 red, 27 green/skipped, 6 still in progress at review end (Test Core 1/2/3/6, Type Check · workspace, Lint & Repo Gates). Dogfood Regression Gate ×3, Dogfood Verify CLI, Build Core, Type Check source/consumer/debt, Check Changeset, Spec property liveness are green. Local read-only re-runs: check-adr-0087-registration --base origin/main --head refs/pm-review/17126 → 0 ([BREAKING+bang] not-required (no-migration-prescription)); check-changeset-no-major → 0.

Findings

F1 — blocking — the refusal is not propagated to the engine: a refused flow is recorded as bound: true, the silent-miss audit skips it, and the engine logs "bound" one line after the trigger logs "NOT BOUND".
packages/triggers/trigger-schedule/src/schedule-trigger.ts:510-518 and time-relative-trigger.ts:254-259 log at error and return from start() without throwing. FlowTrigger.start() is void, so packages/services/service-automation/src/engine.ts:3151-3162 then unconditionally runs this.boundFlowTriggers.set(flowName, …) and logger.info("Flow '<name>' bound to trigger 'schedule'"). Consequences: getFlowRuntimeStates() (engine.ts:3795, Studio's status badge) reports bound: true; getTriggerBindingAudit() (engine.ts:3819, if (this.boundFlowTriggers.has(name)) continue;) omits the flow, so the kernel:bootstrapped audit (service-automation/src/plugin.ts:1130) and the CLI startup summary (cli/src/commands/serve.ts:6429 — documented as "the reliable channel in os dev/os start") both say every triggered flow is wired. That is ruling consequence (3) half-delivered: the stderr line is loud, but every structured surface the repo built for "declared but not armed" affirmatively says the opposite. The dogfood pin (3) only asserts job.has(...) === false plus the log line, which is why this slipped. Fix: throw from start() on a missing organization (the engine's catch at engine.ts:3164-3183 is the designed path for "plugin-supplied thrown text" — it skips the boundFlowTriggers.set, and the audit then lists the flow with binding failed — see earlier warnings), keep the error line, and extend pin (3) to assert getFlowRuntimeStates() shows bound: false and the audit names the flow.

F2 — blocking — published README made false by this PR, not updated. packages/triggers/trigger-schedule/README.md:17-32 (shipped in files[]) shows a start node { schedule: {...}, condition } and states it "auto-launches on that schedule" with context params: { jobId, flowName, schedule }; after this PR that exact flow is refused at bind and the context also carries tenantId. Same for the timeRelative example at :78-96. Per the checklist ("本轮令其变假…的已发布缺陷必修") this is a must-fix in-round. Fix: add organization: '<sys_organization.id>' to both examples and one sentence on the refusal.

F3 — non-blocking — hand-written docs stale. content/docs/automation/flows.mdx:1909-1974 (two type: 'schedule' worked examples, no organization) and content/docs/automation/hooks.mdx:27 / content/docs/permissions/capabilities.mdx:28 (state that a schedule/time_relative flow needs only triggers). The only docs change in the PR is the auto-generated "Misc" reference page. Update flows.mdx examples and add the declaration requirement where schedule flows are introduced.

F4 — non-blocking — the four package-shipped example flows are now permanently refused at boot and this is only an "open question" in the body. examples/app-showcase/src/automation/flows/index.ts:362 (showcase_scheduled_digest, whose header at :350-360 promises "observable end-to-end") and :1660 (showcase_task_due_reminder), examples/app-todo/src/flows/task.flow.ts:7 and :59. Both apps requires triggers (showcase also job). The PR correctly reports that no symbolic organization exists for a package-shipped flow to name, but that is a maintainer decision (companion capability, or a documented "demo needs an org id" step), not something to leave implicit; no dogfood gate asserts on these flows so CI stays green. Needs a maintainer ruling or a follow-up card named in the body; the showcase docstring should at minimum stop claiming it fires.

F5 — non-blocking — three consumer-less exports on the spec barrel, and the "one place" claim is violated. requiresScheduleOrganization, findScheduleOrganizationNearMiss, TIME_TRIGGERED_FLOW_KINDS (packages/spec/src/automation/schedule-organization.ts:100,113,159,170) have zero importers in the tree; reportMissingOrganization re-implements the near-miss scan inline (schedule-trigger.ts:311-314) instead of calling the spec helper. index.ts:44-46 and schedule-organization.ts:176-177 say FlowSchema/"validation" also emit the refusal sentence — they do not (only the two triggers do). Either wire the trigger to findScheduleOrganizationNearMiss, drop the unused exports (the repo's own trigger-schedule/src/index.ts states the rule: "an export whose only consumers live inside its own package belongs in a non-barrel module"), and correct the two comments.

F6 — non-blocking — the fixture comment promises a unit refusal suite that does not exist. packages/triggers/trigger-schedule/src/schedule-trigger.test.ts:50-51 ("a binding without one is refused, which is its own suite below") — there is no such suite; the refusal, near-miss naming, the empty-string/non-string limb of resolveBindingOrganization, and the "hot re-publish removes the key → prior job dropped" stop() limb (schedule-trigger.ts:513-517) are pinned nowhere at unit level and only partially in dogfood. Add the suite (cheap: recording logger + fake job service already exist there) and a spec unit test for schedule-organization.ts (every sibling module in src/automation has one).

F7 — non-blocking — control B on the memory driver is vacuous and its message contradicts its assertion. packages/qa/dogfood/test/schedule-acting-organization.dogfood.test.ts:404-412: on memory the session is deliberately not org-bound (:150), the test asserts only status < 300 with a message claiming it pins a 503 refusal "so the day the driver gains isolation this pin goes RED" — the assertion has the opposite polarity and no delivery check, so the memory half of consequence (5)'s HTTP control certifies nothing. Control A (automation.execute with caller tenantId, both drivers) and control B on sqlite do carry the proof. Fix the message/assertion (assert the real refusal, or state plainly that the memory HTTP control is unavailable and why).

F8 — non-blocking — ScheduleOrganizationSchema lives in a non-.zod.ts file. Every other Zod schema in src/automation is in a *.zod.ts (so its source ships via files[]: src/**/*.zod.ts and the docs generator gives it a page); as a .ts it surfaces in the reference as a "Misc … (no single source file)" bucket (content/docs/references/automation/misc.mdx, references/index.mdx:120). Rename to schedule-organization.zod.ts and regenerate.

F9 — non-blocking — dispatch-required report missing. The dispatch asked the dev to measure whether any deployment/test depends on scheduled runs not landing in sys_automation_run before landing consequence (2), and to report it; neither the PR body nor any commit does. Body still says "verification in progress". Also: unrelated pnpm-lock.yaml esbuild swap (see dependencies above) should be dropped from the diff.

Scope versus card

Executes the ruling (方向一, flow-declared, no fan-out, no fallback limb, no change to any tenancy declaration, NotifyConfigSchema untouched, #17123 explicitly left open). Record-triggered/api bindings leave organization undefined by construction (engine.ts:3063-3090), and control A pins that a declaration cannot overrule a caller's tenant. No silent widening found; the size/xl is dominated by the 439-line pin, TSDoc, and regenerated artifacts. Changeset: present, right three published packages, minor + ! + ADR-0087 not-required (no-migration-prescription) marker, body accurate — matches the launch-window breaking convention. Test/lint validate-flow-trigger-readiness (packages/lint/src/validate-flow-trigger-readiness.ts:596) was not taught the new requirement — acceptable given the "no legal value for package-shipped flows" problem, but it belongs in the same maintainer decision as F4.

Acceptance notes

  • Body first line Fixes #16659 is correct only if F4 is settled here; if the example-flow / symbolic-organization question becomes a follow-up card, the card should still close (the defect is fixed) — keep Fixes.
  • Re-review trigger: F1 and F2 addressed (throw-on-refusal + pin extension; README), F5/F6/F7 comments made truthful, lockfile churn dropped. needs:contract-review stays on both carriers until then.

Generated by Claude Code

…ine records it

`FlowTrigger.start()` is `void`, so logging the refusal and returning left the
engine free to run `boundFlowTriggers.set(...)` and log "bound" one line later:
`getFlowRuntimeStates()` answered `bound: true`, `getTriggerBindingAudit()`
skipped the flow, and the CLI startup summary said every triggered flow was
wired. Both time triggers now throw the same sentence they log, which is the
engine's designed catch path — the flow is never marked bound and the audit
lists it with `binding failed — see earlier warnings`.

Also folds the trigger's inline near-miss scan back into `packages/spec`
(`findScheduleOrganizationNearMissInConfig`, which takes the start-node config
a trigger actually holds), drops the two consumer-less exports
(`TIME_TRIGGERED_FLOW_KINDS`, `requiresScheduleOrganization`), makes the
near-miss vocabulary module-local, renames the module to
`schedule-organization.zod.ts` so its source ships and the docs generator gives
it a page, and corrects the two comments that claimed `FlowSchema` emits the
refusal sentence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
The fixture comment promised a refusal suite that did not exist. It exists now:
the throw itself (the F1 contract), the `error` line carrying the same sentence
the engine's audit points at, near-miss naming, the "hot re-publish removes the
key" stop() limb, per-flow isolation, the no-`error`-channel fallback, and every
limb of `resolveBindingOrganization`. The time-relative sweep gets the same two
load-bearing pins, and `packages/spec` gets the unit test every sibling module
in `src/automation` already had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…l B honest

Consequence (3)'s dogfood pin asserted only that the job service was never
asked. That says nothing about what the ENGINE recorded, which is where the
refusal was being lost: `getFlowRuntimeStates()` answered `bound: true` and
`getTriggerBindingAudit()` skipped the flow entirely. Both are now pinned, each
with the declaring flow as its paired control.

Control B's memory limb asserted `status < 300` under a message claiming it
pinned a 503 refusal — opposite polarity, so it certified nothing. It now states
plainly that the HTTP control is unavailable on this driver and pins the reason
at the seam that makes it so: a tenant-scoped read is refused with
MEMORY_MULTI_TENANT_UNSUPPORTED, so the exemption expires by itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…ished surface

The trigger-schedule README (shipped in files[]) still showed both worked
examples without `organization` and said they auto-launch; after this change
those exact flows are refused. Both now declare the key, and the README states
the refusal and where it shows up.

flows.mdx gains a dedicated "The acting organization" section and both worked
schedule examples declare the key; hooks.mdx and capabilities.mdx point at it so
"needs `triggers`" is no longer the whole story.

The showcase digest's docstring stops promising that it fires: as a
package-shipped flow it has no legal organization to name, no placeholder may be
invented, and what such a flow should do instead is #17150's decision.

Drops the unrelated esbuild@0.28.1/0.28.2 swap from pnpm-lock.yaml; the diff is
now only the link: entry the new dev-dependency needs, and
`pnpm install --frozen-lockfile` leaves it byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…t set

`schedule-organization` now has its own reference page instead of landing in
the "Misc (no single source file)" bucket, and api-surface / export-origins
drop the three consumer-less exports. The changeset's export list matches what
the barrel actually publishes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…w module

Two gate findings the .zod.ts rename brought into scope, both real:
check:spec-parsed-alias only reads *.zod.ts, so `ScheduleOrganization` was
declared with z.infer where ADR-0122 reserves the bare name for the author
state; and llms.txt counts *.zod.ts modules, so the automation domain and the
total were one short.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
`ScheduleOrganizationSchema` is a bare `z.string().min(1)` — no coercion, no
default, no transform — so input === infer and it takes a pin rather than a
permanent `ScheduleOrganizationParsed` synonym. The note records that the
module is not new, only its `.zod.ts` name is: this family of gates reads
`*.zod.ts` only, so the violation sat green behind an extension.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37

os-trump commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Patch round complete — re-review requested at CONTRACT_REVIEW_TIER

domain:services seat #6021. Head 7dc451e65b998f58f3, five appended commits, ⛔ no force-push, no rebase, no amend, no second PR. Clause-② stays yes and needs:contract-review stays on both carriers — ⛔ this seat is claude-opus-5, below tier, and clears nothing.

Against the verdict's own re-review trigger (5602073141)

"F1 and F2 addressed (throw-on-refusal + pin extension; README), F5/F6/F7 comments made truthful, lockfile churn dropped."

state
F1 (blocking) Both time triggers now throw the refusal sentence they log, so the engine's designed catch runs: the flow is never marked bound, getFlowRuntimeStates() reports bound:false, getTriggerBindingAudit() lists it with binding failed. Dogfood pin (3) extended with a (3, structured) case asserting exactly that, on both drivers, each half paired with the declaring flow as control.
F2 (blocking) The published trigger-schedule README declares organization in both worked examples, names tenantId on the context, and states the refusal and the three surfaces that report it.
F5 The trigger's inline near-miss scan now calls the spec helper (renamed findScheduleOrganizationNearMissInConfig, because it takes the start-node config a trigger actually holds — a flow-shaped signature would answer undefined for its only caller's only input). TIME_TRIGGERED_FLOW_KINDS and requiresScheduleOrganization dropped; SCHEDULE_ORGANIZATION_NEAR_MISSES made module-local; both comments claiming FlowSchema emits the refusal corrected.
F6 The promised refusal suite exists — 7 + 8 cases on schedule, 2 on time-relative — plus a new 28-case spec unit test.
F7 The memory control-B limb no longer asserts status < 300 under a message claiming a 503: it states the control is unavailable and pins the reason (MULTI_TENANT_UNSUPPORTED).
F8 Renamed to schedule-organization.zod.ts with its own reference page; the Misc bucket is gone. ⚠️ See below — this rename had a consequence worth the reviewer's eye.
F9 Measured, with two positive controls, and the lockfile churn reverted (pnpm install --frozen-lockfile leaves it byte-identical; the diff vs merge-base is now exactly the three link: lines).
F3 flows.mdx gains an "The acting organization" section plus both examples; hooks.mdx and capabilities.mdx point at it.
F4 Not settled here. #17150 is named nine times in the body; showcase_scheduled_digest's docstring stops promising it fires. No example flow's behaviour changed, no symbolic organization invented, ⛔ no placeholder id written.

⭐ The F1 ablation reproduces the review's own point

Mutation = swallow the throw at the schedule call site, i.e. the reviewed head's exact log-stop-return behaviour. Result: Tests 2 failed | 14 passed (16) — exactly the two new (3, structured) cases, on both drivers — while every other assertion, including the old job.has(...) === false, stayed green. That is the review's finding stated as a measurement: the previous pin could not see it. On-disk proof before the run (anchor 1, injected marker 1, deleted-shape 0, mutated blob ≠ HEAD blob); restore proven by blob equality and an empty git diff HEAD.

⚠️ Two things the reviewer should know that are NOT this PR's to fix

The body corrects its own earlier claim

It previously stated that no platform-organization constant exists in packages/. Withdrawnpackages/runtime/src/http-dispatcher.ts carries PLATFORM_ORG_ID (a private dispatcher sentinel, ⛔ not usable as an organization, but it changes #17150's option-A baseline). ⚠️ Recorded because a wrong measurement in a PR body is read later as evidence.

CI

b998f58f3: 30 success, 4 skipped, Lint & Repo Gates still in progress at the time of writing. Gates locally: 113 derived / 113 run / 0 NOT-MEASURED / 0 UNRUN; five needed a second pass and ⛔ none was reported as a pass — two were real (fixed), two were PREREQUISITE NOT MET on an unbuilt dist (green after pnpm build), and check:type-check-debt timed out at the standard 4096 MB prefix and came back green at 8192 MB, confirming #17151 a second time.

⚠️ Declared rather than hidden: dispatch-gates reports this tree at least 16 commits behind origin/main with six gate-relevant scripts changed in that range. The local derivation is about this tree; CI on the merge is the authority for those six.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation needs:contract-review size/xl tests tooling

Projects

None yet

3 participants