Repository navigation
automation: connector triggers start flows, and a connector action's description / outputSchema reach the flow designer (7 keys) #20287
Description
Activity
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsPath: approvals and automation | 缺项 (no item starts a flow from a connector trigger) | P2
Triage: first grade —
enhancement·priority:p3·domain:spec·area:workflow·pm:queue. Verdict: ENFORCE, by the maintainer's criterionTriage: the readers land in
packages/services/service-automation/src/engine.ts#registerConnector(which walksparsed.actionsonly, :3698) and objectui's flow designer ⇒domain:specparent, with adomain:servicessub-issue and an objectui sub-issue that the spec seat files at dispatch (ruling A′ ②). Rationale: a descriptor that authorstriggersparses clean, although its own docblock says NOT YET ENFORCED (#3197), andoutputSchemais published and ignored. No measured author ⇒ p3.Triage seat (objectstack-wide, seat post #6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T20:31Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), the criterion on #18900 (5727134555), and the #20273 / #20274 / #20282 grades that applied it this week.Verdict. Connector triggers and typed action outputs are mainstream: Power Automate custom-connector polling and webhook triggers with dynamic content, Salesforce Flow with External Services, and Zapier REST-hook triggers. By the criterion ⇒ ENFORCE, 「补消费端(一次做对)」. It is not a decision-box round-trip. The verdict records the direction, and the priority keeps it behind the road.
Execution notes.
- Register
triggersinto the flow trigger registry, with a polling loop and a webhook receiver on the mainstream shape. #getConnectorDescriptorsalready projectsdescription/outputSchema. The objectui designer shows the description and offers the output fields as data tokens.- Each ledger row flips to
live, citing its reader.
- Register
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itenhancementNew feature or requestNew feature or requestand removed
on Sep 27, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · stage 1 only (measure, then report) · 2026-09-28T23:26Z
Session:session_01Sfe5YjBLwB9J3y8fvm2xq1
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-20287-connector-triggers(stage 1 pushes nothing)
Worktree:objectstack-issue-20287
Domain:domain:spec
Seat:domain:spec#5(seat post #19357)
File surface: stage 1 edits no file. It readspackages/spec/src/integration/connector.zod.ts,packages/services/service-automation/src/engine.ts(registerConnector,getConnectorDescriptors, the flow trigger registry),packages/spec/liveness/connector.json, and whatever door those readings lead to. (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tierat397572ed5d: no path-derived mandate;connector.zod.tsis a clause-② SUSPECT path, so the stage-2 PR owes the at-tier review)
Clause-②: no (stage 1 changes nothing; the dev reports the stage-2 arm from the measured surface)
Thread-read: 5859576732
Serial constraints cleared: none. Read at this stamp: the file lists of the 12 open PRs touch none ofconnector.zod.ts,service-automation/src/engine.ts,liveness/connector.jsonorSYNC_ARCHITECTURE.md. #20273 (same schema file, RETIRE) closedcompletedon 2026-09-28. #20281 (same schema file, ENFORCE) is unclaimed.Why this card, from the lane's open unassigned cards read at this stamp:
- P1: [epic] hotcrm ADR-0130 拆包的上游依赖集 —— 一条代码门禁(#18202) + 一次发版,其余五张是作者体验 #18215 is an epic; refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write under
singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204, feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207, feat(spec,drivers,objectql,plugin-security):organization_idNOT NULL per cleared table; one predicate for Layer 0 and every driver; bothorWhereNullarms, the__global__sentinel and the #13491 ledger retire (ADR-0131 D1/D8/D9) — protocol 18 #15212 and feat(spec,objectql,cli): the template install mode — a package copied once into the environment ledger, fully editable, refused on shared-database multi-tenant postures (ADR-0131 D6) #15213 arepm:blocked. - P2: analytics: an authored cube's
public,refreshKey,format,granularitiesand descriptions take effect (8 keys) #20282 stage 2 waits on spec(analytics): retire the innernameon cube measures and dimensions; the record key is the identity (2 keys) #20300 (seat 2's note5869426939); picklist metadata kind — spec:picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 is fenced by its own road-order line; spec: a shared picklist (global value set) metadata kind — option lists reused across objects and packages are TypeScript constants today #18164 is its coordination parent; [Decision] How does a filter say 「is empty」 on a multi-value field? A declared$emptyoperator, or reopen the empty-list refusal (ruling B on #20311, its third arm) #20399 is a Decision card; One artifact, N packages: let a release bundle carry co-owning packages so a product can be split into modules without renaming objects #14122 istracking. - P3: spec(integration): build the connector sync executor that
syncConfigandfieldMappingsdeclare (14 keys), once and on the mainstream shape #20281 (rank 2) asks the maintainer for one word in its own body; spec: the number-comparand refusal says "a declared number field" and "PostgreSQL with a server error" athavingand the per-aggregationfilter, where the column is aggregated and the engine evaluates the clause on every driver #20510 is serial after driver-sql on PostgreSQL answers 500 for a boolean or Date compared against a number field (where { amount: { $gt: true } }), while memory answers no rows and SQLite every row: the non-string half #20336 / #20351 left out #20502; i18n: the flow launcher and runner header readtranslation.flows.<flow>.label(1 key) #20318 waits on a contract-shape fork (seat 4's note5880531686); studio: show the authoredlabel/descriptionon flows, hooks, app areas, RLS policies and the view container (7 keys) #20299's readers are objectui's; spec(ui): retirelist.tabsand the view container's bodyname(2 keys);listViews+ ViewTabBar and the row name already deliver both #20301 stage 2 waits on cloud's writer; [finding] the sixComponentPropsMaprows #20371 added cite about a hundred objectui anchors by line number alone: no quoted first line, so--verify-anchorschecks none of their content #20471 and [finding]check-issue-citationsreads thepre-inpre-#N(andpost-inpost-#N) as a repository qualifier, so a dead number in that spelling is classed cross-repo and never judged #20330 aretooling. This card is rank 5, ahead of realtime: RealtimeEventType names the emitted data.record.* / data.records.* vocabulary (open-core contract half; the transport stays out, #14646 A and #8347 甲) #20288 (rank 6).
Stage 1 asks the dev to measure these, on
origin/main:- How a flow is started today: which trigger types the flow trigger registry holds, and the entry point a connector trigger would join.
- Polling: whether a scheduler exists that a polling loop can ride, and where an outbound poll's credentials come from. If a trigger needs a credential or scheduling contract the spec does not declare, report it; do not invent one.
- Webhook: whether an inbound receiver exists, and how an inbound call is authenticated. A receiver that starts flows is a new inbound surface. Read what spec(integration): retire the connector health-probe, circuit-breaker, authored
statusand nestedwebhookskeys (16), which nothing enforces #20273 retired from the connector's nestedwebhookskeys before relying on them. actions.description/actions.outputSchema: whether any objectstack-side reader is meaningful (for example, a publish-time check of a flow's output references againstoutputSchema), or whether both readers are objectui's only.- A batch plan for stage 2: each batch's file surface, its Clause-② arm, its serial constraints, and every fork that needs the maintainer.
No commit, no PR. The only write is the report comment on this card.
- P1: [epic] hotcrm ADR-0130 拆包的上游依赖集 —— 一条代码门禁(#18202) + 一次发版,其余五张是作者体验 #18215 is an epic; refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write under
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20287, "stage": "1", "status": "needs_decision", "branch": null, "pr": null, "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1", "premise_still_valid": true, "summary": "Measured on origin/main fb386074f5 (2 commits past the seat's 397572ed5d; git diff 397572ed5d fb386074f5 is empty over every path read here). Every factual premise on the card holds. registerConnector walks parsed.actions only (engine.ts:3740). getConnectorDescriptors projects description and outputSchema (:4019/:4021), and nothing in either repo reads them back. The seven ledger rows are dead at the lines the card cites. The description/outputSchema half can be built now as an objectui designer batch: the inputSchema row is live on an objectui-only reader, so the same kind of reader flips both rows. The trigger half cannot be built yet. The flow trigger registry holds one FlowTrigger per KIND, and the kind set is closed at four. No contract exists for a trigger handler, a poll cursor or dedup, or a flow-to-connector-trigger binding. The one inbound receiver authenticates only by a URL token plus an OPTIONAL per-flow HMAC. Accepted ADR-0041 places connector-event triggers (webhook-subscribe and poll) in Tier 3, marketplace/vertical, promotable only by its own rule. Neither the card nor triage cites that ADR. So the trigger direction is a maintainer fork (F1), and F2-F7 follow if the answer is to build.", "tests": "None run. Stage 1 edits nothing: no build, no test, no gate. Every reading is a git grep, sed or jq on the detached worktree at fb386074f5, a REST read of issues, PRs and objectui files, or a depth-1 objectui clone at 797a30f485 in the scratchpad.", "mcp_calls": "0", "api_writes": "1 - this comment: POST /repos/objectstack-ai/objectstack/issues/20287/comments through the fleet-write relay (post-stamped.mjs, transport dispatch)", "measurements": { "m1_how_a_flow_starts": { "registry": "AutomationEngine.triggers is a Map keyed by trigger TYPE and holds ONE FlowTrigger per kind (engine.ts:2395). registerTrigger fills it (engine.ts:3368) and also binds already-registered flows of that type.", "kinds": "Closed FlowTriggerKind = record_change | time_relative | schedule | api (spec automation/flow-trigger-kind.ts:44, FLOW_TRIGGER_KINDS :47). resolveFlowTriggerKind (:64) resolves the kind from the start node config and the flow type. The engine builds the binding per kind in resolveTriggerBinding (engine.ts:3431, exhaustive never-check at :3565) and arms it in activateFlowTrigger (engine.ts:3577) with trigger.start(binding, callback into execute). Flow type enum: autolaunched|record_change|schedule|screen|api (flow.zod.ts:1052). The start node config is an open record (flow.zod.ts:591).", "registered_where": "Four implementations. Method: git grep 'implements FlowTrigger' over *.ts with tests excluded; each known trigger package is hit. ApiTrigger api-trigger.ts:104 (type 'api' :105), registered at trigger-api plugin.ts:75. RecordChangeTrigger record-change-trigger.ts:200 (:201), plugin.ts:64. ScheduleTrigger schedule-trigger.ts:621 (:622), plugin.ts:105. TimeRelativeTrigger time-relative-trigger.ts:265 (:266), time-relative-plugin.ts:105. All four register on kernel:ready.", "manual": "Screen and autolaunched flows resolve to no kind and run through AutomationEngine.execute (engine.ts:5236) from POST /api/v1/automation/NAME/trigger (runtime/src/domains/automation.ts:440, the caller's own identity).", "entry_point_for_a_connector_trigger": "registerTrigger(FlowTrigger), as ONE NEW KIND. The registry holds kinds, not per-connector trigger instances, so 'register a connector's triggers into the flow trigger registry' (card, triage) means: a new FlowTriggerKind member, a resolveFlowTriggerKind branch, a resolveTriggerBinding case (the never-check forces it), one FlowTrigger for the kind, and the defineStack requires refusal plus lint validate-flow-trigger-readiness, both of which read the resolver. The connector side is missing too. registerConnector (engine.ts:3734) checks parsed.actions for handlers (:3740) and stores the whole def (:3748), so triggers stay in the def unread. handlers are ConnectorActionHandler (engine.ts:588), and ConnectorMaterialization is { def, handlers } (spec integration/connector-provider.ts:42-44). No slot for a trigger handler exists anywhere.", "producers_of_connector_triggers": "Zero measured. Method: git grep for intervalSeconds, type: 'polling' and type: 'webhook' over *.ts outside packages/spec, tests excluded. The only hits are driver-turso sync (a different subject) and lint-liveness-properties.ts:496 (the webhook metadata type). The token 'triggers' in packages/connectors hits only CHANGELOG.md; the same token lights up engine.ts this.triggers (control). DeclarativeConnectorEntrySchema refuses triggers on a provider-bound instance (connector.zod.ts:1238-1244). On a catalog descriptor, triggers parse and never reach the registry (header :111-121)." }, "m2_polling": { "scheduler": "YES. IJobService (spec contracts/job-service.ts:192) takes JobSchedule type cron|interval|once with intervalMs (:34-45). service-job implements interval in interval-job-adapter.ts:67 and cron-job-adapter.ts:136. db-job-adapter.ts:251-260 routes interval through the leader-elected cron adapter and warns that every replica fires when that adapter is absent. trigger-schedule already rides it: ScheduleTriggerPlugin depends on com.objectstack.service.job (plugin.ts:51). A poll loop could be one IJobService.schedule(name, interval intervalSeconds*1000, handler).", "dedup": "A persisted claim ledger exists: AutomationEngine.claim(key) over sys_flow_dispatch (engine.ts:2620; FlowDispatchStore :2069). It could key per item as (flow, trigger, item id). No cursor or high-watermark store is declared anywhere.", "gate_and_identity": "OS_AUTOMATION_SCHEDULED_WORK_ENABLED is default OFF (packages/types/src/env.ts:174, :237-240). Its own definition covers 'everything a package ships that fires on a clock rather than on a caller' (:181). The engine gates only schedule and time_relative, though (isTimeTriggeredKind engine.ts:2264, applied :3606), and states that a new kind is OUTSIDE the switch until someone decides (:2255). The time kinds also require a declared acting organization (FlowTriggerBinding.organization, engine.ts:528-547).", "credentials_after_20273": "connector.zod.ts declares two auth keys. First, authentication (:911): the runtime shape with inline secrets (oauth2|api-key|basic|bearer|none, shared/connector-auth.zod.ts:79), supplied by a plugin at registerConnector; the authoring door refuses any non-none value (:1205-1212). Second, auth (:953): ConnectorInstanceAuthSchema, i.e. none|bearer|api-key|basic with credentialRef (connector-auth.zod.ts:118-163). It requires provider and is resolved at materialization by CredentialResolver (service-automation plugin.ts:224; the open-tier env default is at :233) onto ConnectorProviderContext.auth (connector-provider.ts:74). OAuth2 is intentionally absent from instance auth (enterprise tier, ADR-0015; connector-auth.zod.ts:96, :113). #20273 removed health, status and the nested webhooks; nothing about sessions or token refresh remains. retryConfig and requestTimeoutMs are live. So a poll handler that a plugin or a provider builds can close over the credential it already holds. Only static credentials exist in the open tier.", "stop_valve": "Four contracts a polling trigger needs are NOT declared by the spec, so they are reported as forks and not designed here: (1) how a flow selects a connector trigger (F2); (2) the poll handler contract (F3); (3) cursor/dedup (F4); (4) the switch and organization policy for the new kind (F5). A fifth gap is OAuth2 in the open tier (F7). ConnectorTriggerSchema carries key|label|description|type|intervalSeconds only (connector.zod.ts:848-866)." }, "m3_webhook": { "inbound_receivers": "Exactly one receiver starts a flow from an external call: trigger-api's POST /api/v1/automation/hooks/:flowName/:hookId (HOOKS_PATH trigger-api plugin.ts:25). It is mounted at :83 on the RAW Hono app, outside dispatcher routes; its route ledger (trigger-api-route-ledger.ts) records it as server-only. Method: enumerated every rawApp POST|PUT|ALL mount in packages/**/*.ts, tests excluded. The other mounts are cloud-connection, marketplace and plugin-auth. /api/v1/approvals/act acts on an existing approval by one-time token and starts no flow. /api/v1/webhooks/redeliver (webhook-outbox-plugin.ts:396) re-queues OUTBOUND deliveries. Only four FlowTrigger implementations exist (m1).", "authentication_measured": "(1) A hookId path token, compared in constant time. An unknown flow and a wrong hookId both answer 404 (api-trigger.ts:184). hookId defaults to the literal 'default' when the flow sets none (:123). (2) An OPTIONAL HMAC-SHA256 over the raw body in x-objectstack-signature, spelled 'sha256=' plus hex (verifySignature :72-76, checked :187). It is checked ONLY when the flow's start node declares secret. Without one, unsigned posts are accepted and the only signal is a warning at arm time (:151-155). (3) No session or credential check. The only kernel server.use middleware found is the optional inbound rate limiter (runtime/src/dispatcher-plugin.ts:870). The raw-app use() middlewares are server timing (hono-plugin.ts:317), CORS (:417), the hostname guard (cli serve.ts:5541) and the auth-path IP gate (auth-plugin.ts:2169); none authenticates this path. (4) No timestamp or replay window is covered by the signature. x-idempotency-key is sender-supplied and outside the signature (:208), and ingestion is at-least-once through the queue (ADR-0041 section 5). (5) The HMAC secret is a LITERAL read from the flow start node's config.secret (api-trigger.ts:124), so it is stored in flow metadata with no credentialRef indirection. hookId and secret are declared by no spec schema: the start node config is an open record (flow.zod.ts:591), and hookId has 0 hits in spec schema code. (6) Upstream signature schemes are not verifiable by verifySignature (Stripe-Signature and Slack signing both carry a timestamp; GitHub uses X-Hub-Signature-256). This is read from the header name and the format the code checks.", "outbound_is_distinct": "The top-level webhooks: collection goes through bootstrapDeclaredWebhooks (plugin-webhooks bootstrap-declared-webhooks.ts:166) into sys_webhook rows, and the AutoEnqueuer signs each delivery. The signing secret has been persisted ENCRYPTED since #7799 (webhook-secret.ts header). That is outbound delivery and receives nothing.", "what_20273_retired": "The connector's nested webhooks array is now a retiredKey tombstone (WEBHOOKS_RETIRED connector.zod.ts:736, carrier :981). WebhookConfig, WebhookEvent and WebhookSignatureAlgorithm left whole. They were outbound-shaped and never delivered; their prescription points at the top-level webhooks: collection. Nothing inbound survives on a connector. An inbound WebhookReceiverSchema was declared once and removed as never consumed (automation/webhook.zod.ts:105-106). The only declared inbound connector shape is ConnectorTrigger type 'webhook', which has no secret, verification or subscription field." }, "m4_actions_description_outputSchema": { "objectstack_readers": "Only the projection: engine.ts#getConnectorDescriptors (:4006; description :4019, outputSchema :4021), served unchanged by GET /api/v1/automation/connectors (runtime/src/domains/automation.ts:2180-2187). Method: git grep outputSchema over *.ts, *.tsx, *.mts, *.mjs and *.js, excluding packages/spec, tests and dist, gives 11 files. The rest are producers (connector-openapi :216, connector-slack :114/:130/:144, connector-rest :152, connector-mcp :58/:260, showcase connectors :176), a doc comment on flow nodes.outputSchema (lint-liveness-properties.ts:405), four generated form-label translations (platform-objects metadata-forms :2450), and the projection itself. getConnectorDescriptors has two callers: the runtime route, and plugin.ts:1370, which reads names only.", "objectui_readers": "objectui main 797a30f485; FlowReferenceField.tsx, flow-scope.ts and connector-input-fields.ts are byte-identical at the pin dd3f7e1be3. Three fetchers call /automation/connectors (FlowReferenceField.tsx:434 and :472, connector-input-fields.ts:258). They read connectorsToOptions name/label/origin, connectorActionsToOptions key/label (:342), and connectorActionInputSchema key/inputSchema (:117, the lit control). Neither description nor outputSchema is read. nodeOutputRefs (flow-scope.ts:178) offers NO connector_action output: 0 connector_action hits in that file, while the approval branch there is the control. Yet the engine writes each output key as a variable named NODEID.KEY (engine.ts:10159). The other objectui outputSchema hits are AI-tool metadata (ToolPreview.tsx, default-schemas.ts:150).", "is_an_objectstack_check_possible": "Yes, at two doors; neither is needed to flip the ledger. (a) Runtime: AutomationEngine.registerFlow (engine.ts:4160) already runs validateNodeConfigKeys (:9266), validateDecisionModes (:9329) and validateFlowExpressions (:9454). The post-seal soft-fail precedent is sealNodeTypeVocabulary (:9125, called from service-automation plugin.ts:1156), where unknown node types WARN. It sees the live registry, plugin and materialized connectors included. A degraded instance exposes no actions, and outputSchema is JSON Schema by convention only (an open record), so the check could only ever advise. (b) Build time: the @objectstack/lint reference-integrity suite (reference-integrity-suite.ts:119 into validateFlowTemplatePaths :507, run by os validate/lint/compile) today leaves every non-get_record outputVariable and node-id root unjudged. Its only oracle is the stack, and a stack carries action outputSchema only on catalog descriptors: provider-bound entries refuse actions (connector.zod.ts:1231), and plugin connectors are not in the stack. The ledger precedent: connector.actions.inputSchema is live on an objectui-only reader (connector.json:92-99), so the designer reader alone meets the card's acceptance." }, "m5_liveness_rows": { "method": "jq over props.actions.children and props.triggers.children of packages/spec/liveness/connector.json at fb386074f5.", "rows": [ "connector.actions.description :87 dead verifiedAt 2026-09-17", "connector.actions.outputSchema :100 dead verifiedAt 2026-09-17", "connector.triggers.key :116 dead verifiedAt 2026-09-17", "connector.triggers.label :121 dead verifiedAt 2026-09-17", "connector.triggers.description :126 dead verifiedAt 2026-09-17", "connector.triggers.type :131 dead verifiedAt 2026-09-17", "connector.triggers.intervalSeconds :136 dead verifiedAt 2026-09-17" ], "counts": "state-counts.md:68 connector: live 29, exp 0, elsewhere 0, dead 30, planned 1, classified 60. The hand-written README.md:942 partitions the 30 dead by name, including actions.description/.outputSchema and triggers (6).", "drift": "The triggers.type note (:131-135) still says 'see the webhooks subtree below'. Since #20273, webhooks is one leaf tombstone row (:231). This is prose only; any batch that edits the triggers rows carries the fix." } }, "stage2_plan": [ { "batch": "B1 objectui sub-issue: the flow designer reads actions[].description and actions[].outputSchema", "buildable_without_maintainer": true, "file_surface": "objectui packages/app-shell/src/views/metadata-admin/inspectors/: FlowReferenceField.tsx (connectorActionsToOptions :342 carries description into the option); connector-input-fields.ts (an outputSchema finder beside connectorActionInputSchema :99-117); flow-scope.ts (nodeOutputRefs :178 gains a connector_action branch that emits NODEID.PROP per top-level outputSchema property; it is synchronous over node config today, so the fetched descriptor must be passed in); FlowNodeInspector.tsx (wiring); i18n.ts only if new strings; tests beside each file.", "counting_pins": "objectui i18n parity gates if strings are added (NOT MEASURED which gate). No objectstack pin moves in B1.", "clause2": "no. It reads two fields already on the wire; no spec, accept-set or public-surface change.", "serial_constraints": "None measured: none of objectui's 6 open PRs (11025, 11023, 10930, 10777, 10278, 5400) touches these files.", "forks": "none" }, { "batch": "B2 objectstack: flip connector.actions.description and .outputSchema to live", "buildable_without_maintainer": true, "depends_on": "B1 merged and carried by .objectui-sha, so the shipped console holds the reader before the ledger says live.", "file_surface": ".objectui-sha bump plus the manifest regeneration (node scripts/gen-sdui-manifest-node.mjs, AGENTS.md Frontend section), when this PR carries the bump; packages/spec/liveness/connector.json :87 and :100 (status live, evidenceScope cross-repo, evidence citing the objectui symbols at the new sha, producer citing connector-openapi/slack/rest/mcp, same two-door caveat as the actions.key row); .changeset (patch: @objectstack/spec publishes liveness/ per its files[]).", "counting_pins": "packages/spec/liveness/state-counts.md connector row, regenerated with gen:liveness-counts (29/0/0/30/1/60 becomes 31/0/0/28/1/60); packages/spec/liveness/README.md:942, the hand-written dead partition ('sums to 30' and the '29/1/30 split').", "clause2": "no. Ledger and prose only.", "serial_constraints": "#20458 edits liveness/README.md in a hunk at :940-946, adjacent to :942, so a text conflict is certain if both are open; land after it or resolve by hand. #20281 (unclaimed, pm:queue) owns the same connector.json and state-counts connector row (state-counts carries merge=os-regen). Other seats may bump the objectui pin (NOT MEASURED).", "forks": "none" }, { "batch": "B3-retire (only if F1 = B): retire the connector triggers subtree", "buildable_without_maintainer": false, "file_surface": "The #20273 / PR #20350 kit. connector.zod.ts: a triggers carrier tombstone on ConnectorBaseSchema (both published carriers); the DeclarativeConnectorEntrySchema triggers refusal (:1238-1244) deleted; ConnectorTriggerSchema (:848) and ConnectorTrigger to retired defs; header prose. migrations retired-keys entries plus the regenerated registry. conversions: a D2 strip plus a D3 entry, and the triggers[].interval to intervalSeconds rename in connector-health-and-trigger-durations-unit-in-key (conversions/registry.ts:9536) absorbed as monitoringWindow was. retiredAfter stamps, required since #20390. liveness/connector.json: the 6 triggers child rows collapse to one leaf tombstone row. connector-resilience-keys-retirement.test.ts fixtures that author triggers (:293, :301). .changeset with FROM/TO and the ADR-0087 marker. The prescription names what delivers today: an api flow (trigger-api HMAC) for inbound, and a schedule flow whose first node is a connector_action read for polling.", "counting_pins": "state-counts.md connector row (dead 30 to 25, classified 60 to 55); README.md:942 partition; authorable-surface/integration.json (7 lines name triggers or ConnectorTrigger; authorable-surface.base.json only via gen:authorable-surface-base); api-surface, export-origins and declaration-map (ConnectorTrigger exports); type-alias-convention.pin.test.ts (Iso_integration_connector__ConnectorTriggerSchema :941, count minus 1); content/docs/references/integration/connector.mdx (generated); spec-changes.json; protocol-upgrade-guide.", "clause2": "no (narrowing). An authored triggers key becomes a tsc and parse error: breaking, with a migration.", "serial_constraints": "migrations/registry.ts is touched by #20460, #20458 and #20504; conversions/registry.ts by #20458; liveness/README.md by #20458 (:940-946); api-surface/root.json and export-origins/root.json by #20460. #20281 shares connector.zod.ts and connector.json. Use os-regen-merge.sh for every main merge.", "forks": "F1" }, { "batch": "B3-enforce (only if F1 = A): three sequential PRs. B3a is the contract; B3b polling and B3c webhook are runtime.", "buildable_without_maintainer": false, "file_surface": "B3a spec: flow-trigger-kind.ts (a new kind in FlowTriggerKind and FLOW_TRIGGER_KINDS, a resolver branch); flow.zod.ts:1052 (type enum) or a DECLARED start-node binding block; connector.zod.ts ConnectorTriggerSchema binding fields; connector-provider.ts (materialization gains trigger handlers); connector-descriptor.ts (the descriptor projects triggers for the designer); contracts/automation-service.ts; stack.zod.ts requires refusal; lint validate-flow-trigger-readiness. B3b: engine.ts registerConnector refuses a declared trigger with no handler; resolveTriggerBinding case; isTimeTriggeredKind per F5. A new trigger package (ADR-0041 naming: trigger-connector-event) rides IJobService interval plus the claim ledger. serve.ts trigger wiring (:1916-1937) and platform-capabilities.ts:189. B3c: the receiver route plus its route ledger and conformance test (trigger-api-route-ledger precedent); verify and subscribe handlers; queue ingestion (ADR-0041 section 5); new error codes in error-code-ledger.", "counting_pins": "Liveness: 5 triggers rows go live and every new authorable key gets a new row; state-counts.md; README.md:942. authorable-surface integration.json and automation.json; api-surface, export-origins and declaration-map; spec-changes.json; type-alias-convention pin count for new exported schemas. Generated connector.mdx and the flow reference page; platform-objects metadata-forms.generated.ts x4. FLOW_TRIGGER_KINDS pins: flow-trigger-kind.test.ts, flow-trigger-kind-shared-resolver.test.ts, stack-requires.test.ts. check:error-code-casing; docs/qa/platform-checklist items. A new ADR (Tier H, governed) superseding or amending ADR-0041's Tier 3 placement.", "clause2": "yes (widening) for each of B3a/B3b/B3c: new kind, new keys, new exports, a new public route.", "serial_constraints": "The same registry and projection collisions as B3-retire, plus #20460 on stack.zod.ts. B3a before B3b and B3c. The ADR PR is Tier H and lands only by the maintainer's hand.", "forks": "F1, F2, F3, F4, F5, F6, F7" }, { "batch": "B4 (optional): an objectstack-side advisory check of NODEID.KEY references against the action's outputSchema", "buildable_without_maintainer": false, "file_surface": "Door (a): engine.ts registerFlow (:4160) plus the post-seal pass (sealNodeTypeVocabulary :9125). Door (b): lint validate-flow-template-paths.ts (:507) through reference-integrity-suite.ts:119.", "counting_pins": "A new diagnostic code in lint (door b) or a new warn text (door a); no ledger count moves, because B2 already flips the row.", "clause2": "no if it only warns; no (narrowing) if it errors (breaking).", "serial_constraints": "None among open PRs (engine.ts and packages/lint are untouched by all 10).", "forks": "F8" } ], "forks": [ { "id": "F1", "question": "Connector triggers: build them in the open core now (the card's and triage's ENFORCE), or keep ADR-0041's placement?", "evidence": "ADR-0041 (Accepted), docs/adr/0041-flow-trigger-family.md:141-153: 'Tier 3 - marketplace / vertical (future, possibly commercial)': a trigger-connector-event family, 'one package per ecosystem ... supports both webhook-subscribe and poll modes'. Promotion rule: 'a Tier-2/3 trigger moves up when (a) two independent real projects request it, or (b) it unblocks a Tier-1 acceptance criterion. Nothing is built speculatively.' ADR-0097:80 leaves triggers out of scope. The card, both thread comments, connector.zod.ts and connector.json have 0 hits for ADR-0041. Ruling A-prime item 4 on #18900 asks the maintainer for one word per family; triage recorded the direction without that round-trip. AGENTS.md Prime Directive 13: an accepted ADR binds until a superseding ADR says otherwise. Today the flow-level capability is delivered by composition: an api flow (inbound, HMAC) and a schedule flow with a connector_action (poll). Zero producers declare a connector trigger (m1).", "options": [ "A: supersede or amend ADR-0041 (a new ADR, Tier H) and build B3-enforce; F2-F7 then need answers.", "B: keep ADR-0041 and retire the triggers subtree (B3-retire), with a prescription to the api and schedule+connector_action composition; a future trigger-connector-event package re-enters by ADR-0041's promotion rule.", "C: keep the declaration and re-class the rows as planned against ADR-0041 Tier 3. This keeps a declared-but-inert key and conflicts with Prime Directive 10." ], "recommendation": "B. The dispatch did not carry the four-axis framework, so these are the card's own four axes. Real business need: 'an external event starts a flow' and 'poll on an interval' are both deliverable today by composition, and no shipped connector declares a trigger. Long-term soundness: packaging is already decided (Tier 3, per ecosystem); building it into service-automation reverses an accepted ADR without a new one. Preventing AI mistakes: today a descriptor's triggers parse and evaporate; B makes that loud now, while A leaves it inert until B3c lands. Startup-stage restraint: B is one batch on the #20273 pattern; A is a new ADR plus three PRs and five new contracts. Choose A if the maintainer wants connector-owned triggers in the open core now; then F2-F7 apply." }, { "id": "F2", "question": "(Only if F1 = A.) How does a flow name a connector trigger?", "evidence": "The kind set is closed at 4 (flow-trigger-kind.ts:44). The flow type enum has 5 members (flow.zod.ts:1052). The start node config is an open record (:591), and api's hookId and secret are undeclared keys inside it.", "options": [ "A: a new FlowTriggerKind 'connector', flow type 'connector', and a DECLARED strict start-node binding { connectorId, triggerKey }.", "B: reuse the api/schedule kinds with a declared connectorTrigger sub-block; the kind follows ConnectorTrigger.type.", "C: undeclared start-node config keys, as api does today (against Prime Directive 12)." ], "recommendation": "A: one kind, one binding shape, and the engine's never-check forces the runtime case." }, { "id": "F3", "question": "(Only if F1 = A.) What runtime contract carries a trigger's implementation?", "evidence": "registerConnector handlers and ConnectorMaterialization carry action handlers only (engine.ts:588, :3734-3748; connector-provider.ts:42-44). No provider derives triggers, although the refusal at connector.zod.ts:1242 says one does.", "options": [ "A: add trigger handlers (poll: context and cursor in, items and cursor out; webhook: verify and parse, plus optional subscribe/unsubscribe) to registerConnector and the materialization; registerConnector refuses a declared trigger without one, mirroring the action rule.", "B: A for plugins only; keep the provider-bound refusal and correct its message.", "C: provider derivation (OpenAPI 3.1 webhooks/callbacks or x-ms-trigger; MCP has no trigger notion)." ], "recommendation": "B first, which is A scoped to plugins; C deferred." }, { "id": "F4", "question": "(Only if F1 = A, polling.) Where does poll state live?", "evidence": "The claim ledger exists (engine.ts:2620). No cursor store is declared.", "options": [ "A: dedup by item id on the claim ledger, plus a handler-returned cursor persisted in a new sys table.", "B: dedup only (the Zapier shape: return recent items, dedup by id).", "C: the handler owns its own state." ], "recommendation": "B: no new table; the Zapier reference shape." }, { "id": "F5", "question": "(Only if F1 = A, polling.) Does a polling trigger fall under OS_AUTOMATION_SCHEDULED_WORK_ENABLED and the acting-organization rule?", "evidence": "The switch is default OFF and defined as covering all clock-fired package work (env.ts:181, :237-240). The engine gates two kinds only and leaves new kinds outside until decided (engine.ts:2255, :2264).", "options": [ "A: yes to both, the same as schedule/time_relative.", "B: outside the switch, running as system.", "C: under the switch, with the organization taken from the connector instance." ], "recommendation": "A, by env.ts's own definition; the refusal and audit machinery already exists." }, { "id": "F6", "question": "(Only if F1 = A, webhook.) How is an inbound connector call authenticated, and where does the secret live?", "evidence": "m3: the only receiver arms WITHOUT a secret (warn only). The secret is a literal in flow metadata, there is no replay window, and only the x-objectstack-signature format is verifiable.", "options": [ "A: a dedicated receiver that REFUSES to arm without a connector-supplied verify; the secret comes through credentialRef and CredentialResolver; subscribe/unsubscribe lifecycle; queue ingestion.", "B: compile to an api hook with a mandatory HMAC in our header format (upstreams that cannot sign it are excluded).", "C: manual URL paste into the upstream, with no subscribe." ], "recommendation": "A. A receiver that can arm unsigned is exactly the new inbound attack surface this card would add." }, { "id": "F7", "question": "(Only if F1 = A.) Credentials for an outbound poll.", "evidence": "The open tier has static credentials only (none/bearer/api-key/basic via credentialRef). OAuth2 lifecycle is the enterprise tier (ADR-0015; connector-auth.zod.ts:96, :113). Most SaaS trigger APIs need OAuth2.", "options": [ "A: accept static credentials only in the open core.", "B: make connector triggers an enterprise (cloud) capability, matching ADR-0041's 'possibly commercial'.", "C: bring OAuth2 refresh into the open tier (moves the ADR-0015 line)." ], "recommendation": "A if F1 = A. B is F1 = B by another route." }, { "id": "F8", "question": "Should objectstack also check a flow's NODEID.KEY references against the action's outputSchema (B4)?", "evidence": "m4: both doors exist. Neither is required for the ledger flip (the inputSchema precedent). Door (b) has no oracle for plugin or provider actions; door (a) can only advise.", "options": [ "A: no; the designer typing (B1) is the reader.", "B: an advisory warn at registerFlow after the vocabulary seal.", "C: a lint reference-integrity rule over stack-declared descriptors." ], "recommendation": "A. Revisit B only after B1 shows authors reaching for untyped outputs." } ], "open_questions": [ { "question": "The dispatch did not carry the four-axis framework; F1's recommendation uses the four axes written on the card itself.", "options": [ "A: accept this", "B: the seat re-analyses F1 with its own copy of the framework" ], "recommendation": "B, if the seat's copy differs from the card's axes." } ], "out_of_scope_findings": [ "noted, not filed · connector.zod.ts:1242: the refusal tells an author that the provider derives triggers from the upstream (ADR-0097 section 5). No provider derives any: 0 *.ts hits for triggers in packages/connectors; ConnectorMaterialization is def plus handlers (connector-provider.ts:42-44); ADR-0097:79 says actions are derived and :80 leaves triggers out of scope. The refusal is right; its reason is wrong. · class: none of a/b/c by itself · carrier: this card's stage-2 trigger batch (B3-retire deletes the rule, B3-enforce rewrites it)", "noted, not filed · the trigger-api HMAC secret is a literal read from the flow start node's config.secret (api-trigger.ts:124). The showcase says 'real deployments inject this from configuration' (examples/app-showcase/src/automation/flows/index.ts:1581), but no injection seam exists: the api binding passes the raw start-node config (engine.ts:3551). Outbound sys_webhook secrets, by contrast, are encrypted (#7799). · class: candidate (a), NOT MEASURED: no read-path probe (whether GET /api/v1/meta/flow/NAME serves config.secret to a non-admin persona) · dedupe words: trigger-api secret cleartext, flow start node config.secret, x-objectstack-signature, hookId default · carrier: none unless F6 = A" ], "not_measured": [ "cloud repo: GET /repos/objectstack-ai/cloud answered 403 to this session, so whether cloud ships a trigger-connector-event package, OAuth2 connection lifecycle or any connector trigger is unknown.", "check-liveness.mts --json was not run (no install in the measuring worktree). Statuses were read from connector.json with jq; counts come from the committed, generated state-counts.md.", "No live HTTP probe of POST /api/v1/automation/hooks/... (unsigned acceptance, absence of a session requirement). Read from source, the route ledger and middleware registrations only.", "Whether a flow's start node config.secret is readable through the metadata read API by a non-admin persona.", "Whether check-liveness existence-checks a cross-repo (objectui) evidence pointer.", "Which objectui gates count what B1 adds (i18n parity and the like).", "Whether the maintainer gave this family its one word under ruling A-prime item 4 anywhere outside this card's thread.", "scripts/adr-anchors entries for ADR-0041 were not read." ] }Measured at 2026-09-28T23:45Z on origin/main fb38607 (stage 1: measure and report only; no commit, no push, no PR).
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actions裁定与请示:第一阶段测量之后(连接器触发器的方向,需维护者一个字母)
domain:specseat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1),2026-09-28T23:55Z。复核对象:dev 报告5880862266。一句话: 连接器上现在可以写"触发器"(例如"Slack 来一条消息就启动流程"),但写了什么都不会发生。要不要现在就在开源核心里把它做出来?A 现在做;B 先把这个不生效的写法拆掉,等有真实客户要再按插件做。本席推荐 B。
复核(本席在
fb386074f5上重读)registerConnector只遍历parsed.actions(engine.ts:3740);getConnectorDescriptors投影了description与outputSchema(:4019、:4021)。- 流程触发种类闭合为四种(
packages/spec/src/automation/flow-trigger-kind.ts:44)。 - 台账七行均为
dead(connector.json:87/100/116/121/126/131/136)。 - objectui
797a30f485:connectorActionsToOptions(FlowReferenceField.tsx:342)不读description;nodeOutputRefs(flow-scope.ts:178)没有connector_action分支;inspectors/目录里outputSchema0 命中。 - ADR-0041 的原文见下方 Governing text。
- 未复核:报告对 raw-app 中间件的枚举;cloud 仓(报告自述读取 403)。
本席裁定(不上交)
- 动作这一半(
description/outputSchema)按 triage 的 ENFORCE 做,不等触发器的决定。- B1:objectui 子卡 flow designer: show a connector action's description in the picker, and offer its outputSchema properties as downstream references (the objectui half of objectstack#20287) objectui#11028 已开(不认领、不打标;本席不执行 objectui 工作)。
- B2:B1 落地、objectstack 的
.objectui-sha带上之后,本卡把这两行翻为live。liveness/README.md:942与在飞 PR feat(spec)!: retire the inner name on cube measures and dimensions — the record key is the member's name (#20300) #20458 改的 :940-946 相邻,B2 排在它之后。 - Clause-②:B1、B2 都是
no(B1 只读线上已有的两个字段;B2 只改台账和说明文字)。
- F8(objectstack 侧再加一道输出引用检查):不做。
inputSchema那一行就是只靠设计器读者翻成 live 的先例;新增门禁默认否。 - 报告的出界发现 2 已开成 trigger-api arms a flow's inbound hook without a secret and accepts unsigned posts; ADR-0041's trigger-api acceptance criteria name a per-flow secret and HMAC verification #20529:没配密钥时,trigger-api 也接受未签名的请求,而 ADR-0041 的验收条写的是每个流程一个密钥,加签名校验。不认领、不打标,交 triage。出界发现 1(
connector.zod.ts:1242的拒收理由不对)随触发器那一批处理。
请示:连接器触发器(报告 F1)
Governing text(两条方向相反):
- ADR-0041(Accepted),
docs/adr/0041-flow-trigger-family.md:141-153:连接器事件触发器(webhook 订阅与轮询)列在 "Tier 3 — marketplace / vertical (future, possibly commercial)";晋级规则是 "two independent real projects request it, or (b) it unblocks a Tier-1 acceptance criterion. Nothing is built speculatively." - [Decision] Route declared≠enforced work by the SEAM, not the layer — a
Seam:line on filing, vertical dispatch by default in the spec lane, automatic parent + sub-issues for spec↔objectui seams, Journey as a filter, bulk retirement per spec family, Console Pin Gate back to required (the maintainer's 「同意」 on the five-line batch, 2026-09-18) #18900 裁决 A′ ④(5727134555):「主流平台有没有这个能力 —— 有 ⇒ 补消费端(一次做对);没有 ⇒ 退役」,并且每一族由维护者答一个字。 AGENTS.md:244(Prime Directive 13):已接受的 ADR 在被新 ADR 取代之前一直有效。- triage 的判定
5859576732按 A′ ④ 记为 ENFORCE,但没有引用 ADR-0041(卡正文与该评论里0041均 0 命中)。两条规则在这一族上方向相反,另外新开一个对外入站入口属于安全边界,所以交您定。
前提(每条带复查命令):
- ADR-0041 把这族放在第三档:
sed -n '141,153p' docs/adr/0041-flow-trigger-family.md - 本仓没有作者声明连接器触发器:
git grep -n -E "^\s*triggers\s*:" -- '*.ts' '*.tsx' '*.json' ':!packages/spec/**' ':!*.test.ts' ':!**/dist/**'→ 15 处命中,逐条读过,没有一处是连接器触发器:webhook 的triggers(示例、plugin-webhooks、两个测试夹具)、serve.ts:1911的插件分组、技能表单triggers的四份翻译,以及plugin-webhooks对象字段的四份翻译。范围只到本仓。 - 连接器注册只读动作:
sed -n '3734,3748p' packages/services/service-automation/src/engine.ts - ADR-0097 也把
triggers放在范围之外:sed -n '80p' docs/adr/0097-*.md
选项与代价:
选项 做什么 客户看得到的后果 A 现在做 先写一份新 ADR 改掉 ADR-0041 的档位(受管面,由您亲手合),再建触发器;报告列出的 F2–F7 共六个具体问题要您逐个定(流程怎么指定触发器、触发实现的接口、轮询状态放哪、定时开关、入站鉴权与密钥、轮询凭据) 连接器能直接"被事件叫醒";平台多一个对外入站入口 B 先拆掉 保留 ADR-0041;把连接器上的 triggers整族退役(破坏性,但本仓零作者)。写了会在解析时被明确拒绝,并指路:外部事件用api流程(它的签名目前可选,见 #20529),定时拉取用schedule流程调连接器动作。将来按 ADR-0041 的晋级规则,以独立触发器包的形式回来今天写了不生效的东西,变成写了就报错并告诉你怎么做 业务上等于什么:
- A = 现在就把"连接器自带触发"做成开源核心功能,像 Power Automate 自定义连接器那样。
- B = 这项能力按原计划放进"生态插件,可能收费"那一档,有真实客户要了再做;先把现在这个假按钮拆掉。
四轴(业务立场):
- 实际业务需求: 本仓零作者(见前提)。"外部事件启动流程"和"定时拉取"今天都能靠
api流程、schedule流程加连接器动作组合出来(报告 m2、m3 的读法)。 - 项目长远合理性: 两年后的样子按主流平台看:Power Automate 自定义连接器的
x-ms-trigger、Zapier 的 REST hook 与轮询,都是连接器自带触发器,并带订阅/退订、签名校验和去重。现在这个声明形状(key/label/description/type/intervalSeconds)没有这些字段,所以选 A 也得重新设计,不是把现有键接上就行。归属 ADR-0041 已经定了:按生态分包,放第三档。 - 防 AI 写错: 今天 AI 写了
triggers能通过解析,运行时悄悄什么都不做,出错时没人看到。B 让它在解析时立刻报错并指路。A 要等整族建完才不再静默。 - 创业阶段不扩散: 零拉动,默认不扩散。B 去掉一个永久义务;A 新增一族契约和一个入站安全面。
os-decision-facets
- ① 长远合理性:B 缩小契约(删掉一族没人读的声明);A 扩大(新触发种类、新接口、新入口)。
- ② 业务拉动:本仓零作者;零拉动默认移除。
- ③ 防 AI 犯错:B 在解析时响亮拒绝;A 在建完之前一直静默。
- ④ 不扩散:B 去掉一个永久义务;A 增加一族。
- Prior rulings read:
0041,trigger-connector-event,ConnectorTrigger, [Decision] Route declared≠enforced work by the SEAM, not the layer — aSeam:line on filing, vertical dispatch by default in the spec lane, automatic parent + sub-issues for spec↔objectui seams, Journey as a filter, bulk retirement per spec family, Console Pin Gate back to required (the maintainer's 「同意」 on the five-line batch, 2026-09-18) #18900 A′ ④ → ADR-0041 Tier 3(:141-153)、ADR-0097 §5(:80)、裁决5727134555;thread: 2(5859576732、5880862266)。
推荐 B。 只看①选 B:现有形状不是两年后的形状,而终态的归属 ADR-0041 已经定了。②③④ 是否翻转:否,三轴同向。
回退 A: 如果您要把连接器触发器放进开源核心,本席先起草一份取代 ADR-0041 相关段落的新 ADR(草稿 PR,由您合),再把 F2–F7 一次呈给您。
置信缺口: 看不到 cloud 仓和客户那边是否已经有两个项目在要;也看不到您对开源和收费边界的打算(ADR-0041 写的是"可能收费")。裁后执行:
- 选 B:本卡开一个退役 PR。
triggers整族转为退役键墓碑,指向上面两种组合;台账里触发器那五行随之退役;connector.zod.ts:1242的拒收理由一并改正。带 BREAKING 横幅(Clause-②: no (narrowing)),走在档审查。 - 选 A:按上面的回退执行。
卡片状态:
pm:dispatched→needs-user-decision(一笔替换)。认领人保留:动作那一半(B2)仍由本席接着做,等 objectstack-ai/objectui#11028 落地。
Generated by Claude Code
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsRuling: batch #242 item 1 · letter B · maintainer 「同意」 2026-09-29T01:22Z
Director seat (objectstack#12708, summon #30 续 2,
session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-29, replying 「同意」 to the presented batches #240, #241 and #242 together — this item recommendation B, fallback A, the full four-axis body in chat on 2026-09-29.Ruled: B. The connector
triggersfamily (ConnectorSchema.triggers→ConnectorTriggerSchema:key/label/description/type/intervalSeconds) is RETIRED now under ADR-0049 enforce-or-remove; ADR-0041 stays as it is (trigger-connector-eventat Tier 3, the two-real-projects promotion rule). The retirement is aretiredKey()tombstone onConnectorSchema.triggerswhose prescription names the two shapes that work today: an external event starts anapiflow that calls the connector's action; a scheduled pull is ascheduleflow that calls the connector's action. A (build connector triggers into the open-source core now, behind a new ADR re-tiering ADR-0041, then the six design questions F2–F7) is not taken. When the promotion rule is met, the family returns as its own trigger package in the mainstream shape — subscribe / unsubscribe lifecycle, signature verification, a dedupe cursor — which the present five keys cannot carry.The action half of this family is unchanged by this ruling: the seat's own disposition stands (objectui#11028 gives
description/outputSchematheir designer readers; the two ledger rows flipliveafter the pin bump; no objectstack output-reference gate). #20529 (the trigger-api inbound hook armed without a secret) is triage's.Readings that decided it:
- Long-term: ADR-0041 is Accepted and holds until superseded (Prime Directive 13); 「一次做对」 means the mainstream shape re-declared on promotion, not five placeholder keys wired up; ADR-0097 §5 already keeps
triggersout of scope;registerConnectorwalksparsed.actionsonly (engine.ts:3740) and the flow trigger kinds are a closed set of four. - Real use: zero in-repo connector-trigger authors (the one
triggers:hit outside spec is a webhook-object fixture); external events and scheduled pulls compose fromapi/scheduleflows plus connector actions today. - AI-safety: today an authored
triggersparses clean and does nothing; the tombstone refuses at parse and names the two working shapes; A stays silent until the whole family exists. - Startup scope: B removes a permanent obligation; A adds a contract family and an inbound security surface.
Execution parameters (ruled in the same stroke):
domain:specseat 5 keeps the card and opens the retirement PR perspec-property-retirement: the tombstone onConnectorSchema.triggerswith the prescription above; ADR-0087 D2 conversion (protocol 18, retired from the load path, lossless — nothing ever read the key) strippingtriggersfrom stored connector rows, plus its D3 semantic entry;RETIRED_KEYS_BY_MAJOR[18]per the registry's convention; the five trigger rows inpackages/spec/liveness/connector.json(:116–:136) retired; the refusal reason atconnector.zod.ts:1242corrected; docs and skill mentions swept;Clause-②: no (narrowing);@objectstack/specminor with the BREAKING banner; at-tier contract review.- ADR-0041 untouched; no new ADR.
- Confidence gaps recorded: cloud / customer demand for connector triggers (the promotion rule's two projects) is not visible from here; the open-source vs. commercial boundary is ADR-0041's 「possibly commercial」 as written.
State:
needs-user-decision→pm:dispatched(the seat's live claim; assigneeos-justinstays); the Ruled line goes on the body in the same stroke.- Long-term: ADR-0041 is Accepted and holds until superseded (Prime Directive 13); 「一次做对」 means the mainstream shape re-declared on promotion, not five placeholder keys wired up; ADR-0097 §5 already keeps
29 remaining items
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 20 (B2, folded with #20299's remaining four rows into one dispatch) · 2026-09-30T07:11Z
Session:session_01Sfe5YjBLwB9J3y8fvm2xq1
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-20287-20299-ledger-flips-at-pin
Worktree:objectstack-issue-20287-20299
Domain:domain:spec
Seat:domain:spec#5(seat post #19357)
Clause-②: no (ledger rows only; no accept set moves)
Remainder: B2 from the stage-2 landing record5888771371.actions.descriptionandactions.outputSchemagoliveinpackages/spec/liveness/connector.json, citing the flow-designer reader (objectui#11028, PR objectui#11084) at the pindb11afd4967c. The unlock note5905157674confirmed that pin carries it.
Fold: the unlock note suggests a fold with #20299 (four rows, the same remainder shape), and the seat takes it. One PR closes both cards. Each row is checked on its own at the pin, and each ledger's row is independent.
File surface (stop on breach and explain it in the report):packages/spec/liveness/connector.json: the twoactions.*rows only.- The state counts and liveness README rows that these flips move, regenerated only.
.changeset/20287-*.md:@objectstack/specpatch, becauselivenessships in the package.- ⛔ No other ledger row, and no
packages/spec/src/**. The dead tracker numbers in these ledgers belong to packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234's slices.
Container & model:M,mode:subagent,model: opus.
Review: an at-tier contract review is owed (.changesetprose).
Thread-read: 5905157674
Serial constraints cleared: read at this stamp, no open PR touchesconnector.json. packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234's liveness slices avoidconnector,app,permissionandviewuntil this PR lands.
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20287, "folded_issues": [ 20299 ], "status": "done", "branch": "claude/issue-20287-20299-ledger-flips-at-pin", "pr": "https://github.com/objectstack-ai/objectstack/pull/20814", "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (subagent; the parent seat's harness-stamped id)", "premise_still_valid": false, "summary": "PR #20814 (draft, head 41b5470514, base 96e724475c) moves 3 of the 6 dispatched rows from dead to live. Each flipped row names its objectui reader and producer, read at the .objectui-sha pin db11afd4967: connector.actions.description, connector.actions.outputSchema and app.areas.description. The premise that the pin carries a reader is FALSE for the other 3 rows, which stay unchanged. (1) permission.rowLevelSecurity.label and .description: PermissionPreview renders both, but no production route mounts it for permission. The route metadata/:type/:name goes to MetadataResourceEditPage, and that dispatcher returns the registered custom EditPage PermissionMatrixEditPage (services/builtinComponents.tsx) for every non-create permission item. That page renders no preview. Its RLS facet, PermissionAdvancedFacets, reads no policy label or description. Create mode shows previews only for object, report and dataset. The only other callers of getMetadataPreview are EmbeddedItemEditor (editAs is field, index or validation only), StudioDesignSurface (surfaces page, object, dashboard, report, action and flow; never permission) and apps/console preview-gallery (dev-only, excluded from the production build). (2) The view container label: ViewPreview#ViewLabelHeading draws draft.label, but the Studio view draft is always a ViewItem. getMetaItems in metadata-protocol protocol.ts drops the aggregated container from every view enumeration, and Studio's view listFilter drops it again. expandViewContainerWithDiagnostics stamps each ViewItem's label from its list or form entry (v.label), never from the container's own label. So the container label reaches no enumerated Studio surface. The view.name row draws the same line: a ViewItem read is not a container read. Because #20299 keeps 3 open rows, the PR body says 'Part of #20299' and NOT the dispatched 'Closes #20299'. os-dev.md: use Part of when the merge should not close the card. Conflict flagged here, not silently chosen. FILE-SURFACE EXTENSION, declared: my flips made 4 hand-written sentences false, and each is corrected minimally. They are the app and connector README Notes cells (the dead enumerations and counts), the connector.actions.inputSchema note ('The one action key with a structural consumer') and the app.areas container note ('description is the surviving benign dead key'). This follows the os-dev rule that a change which makes published text false must fix it; the dispatch's file surface said generated-only. The rows left unflipped are untouched, per the dispatch. Their old notes are now stale: the RLS note still says the preview renders only a count and is reachable, and both halves are now false. See out_of_scope_findings.", "row_verdicts": [ "connector.actions.description: LIVE. reader objectui @db11afd4967 packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx#connectorActionsToOptions (description becomes option hint) + #ReferenceCombobox (datalist draws 'label — hint'); mounted by inspectors/flow-node-config.ts#FLOW_NODE_CONFIG (connector_action actionId is a connector-action reference) under FlowNodeInspector, registered for flow in inspectors/index.ts. The reader is fed by useConnectorActionOptions from GET /api/v1/automation/connectors, which framework engine.ts#getConnectorDescriptors projects (description: a.description). Producers: connector-slack createSlackConnector, connector-openapi createOpenApiConnector, connector-mcp createMcpConnector. The two-door caveat is the same as for the sibling actions.* rows. How checked: git show of each file at db11afd4967, with the flow-designer files byte-unchanged between the objectui#11084 merge a5841be351 and the pin (git diff --stat a5841be351 db11afd4967 lists none of them).", "connector.actions.outputSchema: LIVE. reader objectui @db11afd4967 inspectors/flow-scope.ts#nodeOutputRefs (connector_action branch, via connectorActionOutputSchema and connectorActionOutputKeys, one nodeId.key ref per top-level properties key) and #resolveFlowScope. Producer of the input the read depends on: connector-input-fields.ts#useConnectorRegistry, called by FlowNodeInspector.tsx#FlowNodeInspector and FlowEdgeInspector.tsx#FlowEdgeInspector and passed to useFlowScope. Framework: engine.ts#getConnectorDescriptors (outputSchema: a.outputSchema); slack slackOutputSchema, openapi buildOutputSchema, mcp tool.outputSchema. Engine output storage checked: engine.ts sets variables under node.id + '.' + key for each result.output key.", "app.areas.description: LIVE. reader objectui @db11afd4967 previews/AppPreview.tsx#readAreas (resolveI18nLabel of area.description) + #AppPreview (Areas list, rendered outside the designMode branch). Producer: previews/index.ts#registerBuiltinPreviews registers AppPreview for app, register-builtins.ts calls it, and the app-shell index imports that file. ResourceEditPage.tsx#MetadataResourceEditPageImpl resolves getMetadataPreview(type) and renders PreviewComponent with draft. app has no custom EditPage: the only EditPage registration in the pin tree is permission's. Framework: rest meta-item-read-gate.ts#filterAppForUserWithReason serves areas with every area key (filterAreas spreads each area).", "permission.rowLevelSecurity.label: NOT FLIPPED (unchanged). Reader exists: PermissionPreview.tsx#readPolicies and the RLS Section. It is unreachable: ResourceEditPage.tsx#MetadataResourceEditPage returns the custom EditPage, and builtinComponents.tsx registers EditPage: PermissionMatrixEditPage for permission. registry.ts#registerMetadataResource merges configs, so the anchors.ts permission registration keeps that EditPage. Whole-tree git grep of getMetadataPreview at db11afd4967 found 4 production callers and none of them serves permission. PermissionAdvancedFacets.tsx reads rowLevelSecurity but no policy label or description. Static closure only; not booted.", "permission.rowLevelSecurity.description: NOT FLIPPED (unchanged). Same closure as label.", "view.label (container): NOT FLIPPED (unchanged). ViewPreview.tsx#ViewLabelHeading reads draft.label. Every enumerated draft is a ViewItem: metadata-protocol protocol.ts#getMetaItems filters isAggregatedViewContainer out of view lists, and the Studio view listFilter in builtinComponents.tsx does the same. spec view.zod.ts#expandViewContainerWithDiagnostics copies v.label and defaultList.label, never the container label. The walker governs the container member of the view union (check-liveness.mts shapeOf; ViewItem is skipped). The view.name row already refuses a ViewItem read as a container read. Seat 4's 2026-09-28 measurement (5880539559) named this same obstacle: the view list drops the aggregated container." ], "compare_readings": "REST compare a5841be351...db11afd4967c (objectui#11028 via PR objectui#11084): 200, status ahead, ahead_by 23, behind_by 0, merge_base a5841be35189. REST compare 5b2ea17570...db11afd4967c (objectui#11027 via PR objectui#11098): 200, status ahead, ahead_by 17, behind_by 0, merge_base 5b2ea1757082. Local git merge-base --is-ancestor on the (shallow) objectui object store: exit 0 for both, which self-proves. .objectui-sha on main (REST contents): db11afd4967cd9d39381c5e21dc2deec9d706204. Both premises hold. The merges are in the pin; what failed for 3 rows is reachability, not the pin.", "tests": "All at head 41b5470514 (base 96e724475c) unless stated. packages/spec check:liveness: exit 0. connector: 55 classified (live 31, planned 1, dead 23). app: 59 classified (live 50, dead 8, planned 1). permission 36/6 and view 78/11 unchanged. Symbol anchors 830/830. Key-mention: 662 asked, 661 anchored, 1 exempt. Shard sum: 960 live, 122 dead (was 957/125). gen:liveness-counts: exit 0, '2 shard(s) rewritten (app.md, connector.md)'. Spec build through os-verify-lock: 'VERDICT command-exit 0'. check:generated: exit 0. vitest --project local scripts/liveness (lock): Test Files 10 passed, Tests 263 passed. vitest --project repo count-shards-merge + 3 connector retirement tests (lock): Test Files 4 passed, Tests 63 passed. dispatch-gates --commands derived 60, all run and reconciled with --ran and exit codes: 57 exit 0; 1 exit 1; 2 exit 3. The exit 1 is pnpm check:platform-checklist, which fails on docs/qa/platform-checklist/areas/identity-auth.json ABSENT SYMBOL packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor. The diff touches none of its inputs, the gate is outside per-PR CI, and platform-checklist-watchdog.yml owns a main red. The two exit 3 are NOT MEASURED: check:dual-build-cjs-loads and check:lean-entry-closure answer PREREQUISITE NOT MET (need whole-workspace dist; the diff changes no package entry point). NOT MEASURED: @objectstack/lint lint-liveness-properties. Reason: lint warns only on authorWarn, experimental or planned rows, and none of the 3 rows carries authorWarn before or after, so the warning set cannot move; CI Test Core runs it. Typecheck: no TS in the diff; CI lanes run it. Control-byte self-scan of the 4 edited text files: none. check:nul-bytes: OK. No ablation: data-only diff, and the gate's own symbol-anchor and key-mention checks are what verify the new pointers. CI snapshot of 41b5470514, taken once and not waited on: 32 check-runs deduped by name. 14 success. 15 in_progress (Lint & Repo Gates, Test Core 1-6, Type Check workspace and consumer gates, Dogfood 1-3, Dogfood Verify CLI, Build Core, Temporal Conformance). 3 skipped: Build Docs (ci.yml filter docs=false, no docs paths), Console Pin Gate (filter console=false, .objectui-sha unchanged), Packed-tarball smoke (opt-in, label needs:pack-smoke absent).", "mcp_calls": "0", "api_writes": "4 REST writes, each through the fleet-write relay as objectstack-fleet[bot]. Each is one POST /repos/objectstack-ai/objectstack/dispatches under the session token. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft forced), relay run 36688923865 success, PR #20814, body read back byte-identical. (2) label-write --assign os-justin: POST /repos/objectstack-ai/objectstack/issues/20814/assignees, relay run 36689001361 success, read back assignee os-justin (labels documentation, size/s and tooling came from the labeler, not this write). (3) and (4) post-stamped: POST /repos/objectstack-ai/objectstack/issues/20287/comments and /issues/20299/comments, this report. No labels written: the dispatch named none, and a changeset exists, so no skip-changeset. Also 2 git pushes (not REST).", "open_questions": [ { "question": "view.label: does the ledger row govern only the defineView container's own top-level label (the walker's shape), or the top-level label of any view item, including the ViewItem that Studio authors and ViewPreview draws?", "options": [ "A: container only. The row stays dead. It flips when a Studio surface draws a container's own label, or the key is reconsidered. The ViewItem top-level label then has no row of its own, which is a ledger-coverage note.", "B: any view item. Flip it live citing ViewPreview#ViewLabelHeading. Under B the row was already live before objectui#11027 (the Studio view list registers a Label column), and view.name would need the same re-grade, because its dead verdict rests on the container/ViewItem split." ], "recommendation": "A. The walker (check-liveness.mts shapeOf) and the same file's view.name row both draw the container/ViewItem line. B would re-grade two rows on a ledger-reading change, not on objectui#11027." }, { "question": "permission.rowLevelSecurity.label/.description: PermissionPreview draws both at the pin but is never mounted for permission. How should #20299's remaining permission half proceed?", "options": [ "A: an objectui follow-up either mounts PermissionPreview (or an RLS policy list) inside PermissionMatrixEditPage, or renders label and description in PermissionAdvancedFacets' policy list. Then a pin bump, then these rows flip. A booted Studio check first confirms the unmounted preview.", "B: count the registered-but-unmounted preview as the reader. The README's rule refuses this: 'a preview no registry hands a draft to is a read point that never runs'." ], "recommendation": "A. B would repeat the 2026-08-10 error in which the RLS rows' note called PermissionPreview reachable." } ], "out_of_scope_findings": [ "carrier: #20299 (stays open; the seat routes the objectui half) · noted, not filed. objectui @db11afd4967: PermissionPreview is registered for permission (previews/index.ts) but no production route mounts it, because the custom EditPage PermissionMatrixEditPage takes metadata/permission/:name. objectui#11027's permission half therefore reaches no Studio user. Class a candidate. reach: NOT MEASURED booted; static call-graph closure only (registration, accessor, all 4 production callers), which is not a filing-grade reach. dedupe words: PermissionPreview, PermissionMatrixEditPage, rowLevelSecurity label, registerMetadataPreview permission, getMetadataPreview", "carrier: #20299 · noted, not filed. The ledger has no row for a ViewItem's top-level label or viewKind: check-liveness.mts shapeOf walks only the container member of the view union and says ViewItem config is covered via list/form. The standalone ViewItem label that Studio's create form requires is ungoverned as its own coordinate. Ledger-coverage note, no reach. dedupe words: ViewMetadataSchema union walker, ViewItem label liveness, shapeOf union container", "carrier: #20299 · noted, not filed. The unflipped rows' notes are stale in both directions and were left as-is per the dispatch. permission.rowLevelSecurity.label/.description still say PermissionPreview renders only a count and is reachable (ResourceEditPage.tsx:949). At the pin it renders label and description but is not mounted. view.label still says the only obstacle is the one-entry tab strip. The heading now exists, and the real obstacle is that the container never reaches the preview.", "carrier: platform-checklist-watchdog.yml (standing main-red anchor) · noted, not filed. pnpm check:platform-checklist exits 1 at 96e724475c-based head: docs/qa/platform-checklist/areas/identity-auth.json cites packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor, which no longer resolves. Not per-PR CI. Inputs untouched by this diff. dedupe words: identity-auth.json twoFactor ABSENT SYMBOL" ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsRuling on the fold report (
5907283710on #20287,5907294566on #20299), and round 2 · 2026-09-30T08:30Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claims5906102916(#20287) and5906110970(#20299). Draft PR #20814 at41b5470514.Accepted as delivered:
- Three rows go
live:connector.actions.description,connector.actions.outputSchemaandapp.areas.description. Each cites its reader and producer at the pindb11afd4967, and each compare readsbehind_by 0. - Three rows stay
dead:permission.rowLevelSecurity.label,permission.rowLevelSecurity.description, and theviewcontainerlabel. No reader at the pin reaches them. The seat checked the dev's two key facts at the pin:builtinComponents.tsx:187registersEditPage: PermissionMatrixEditPage.getMetadataPreview(has production callers only inResourceEditPage,EmbeddedItemEditorandStudioDesignSurface; the fourth isapps/console/src/preview-gallery.tsx.builtinComponents.tsx:232filtersisAggregatedViewContainerout of the view list.
Part of #20299, not the dispatchedCloses. This is right: three of its rows stay open.- The declared surface extension: four hand-written sentences that the flips made false are corrected, and this is the minimum. The rule that a change must not leave text it made false wins over the claim's "generated only".
Open question 1 (
view.label): A. The row governs the container's own top-levellabel, which is the walker's shape, and it staysdead. Theview.namerow in the same file draws the same line, and B would re-grade two rows because of how the ledger is read, not because of objectui#11027.Open question 2 (the permission half): A. An objectui follow-up either mounts a preview for
permissionor draws the policylabel/descriptionin the RLS facet. Then comes a pin bump, and then these two rows flip. Its first acceptance is a booted Studio check that confirms the preview is unmounted, because the finding so far is a static closure. The seat files it when this PR lands, and #20299 then goespm:blockedon it.Round 2 is note text only, on the three rows that stay
dead. Their notes are now false both ways (the report's third out-of-scope finding):- The RLS notes say
PermissionPreviewrenders only a count and is reachable. At the pin it renderslabelanddescription, but no route mounts it. - The
view.labelnote names the one-entry tab strip as the obstacle. The obstacle is that the container never reaches the preview.
Each note says what holds at the pin, and the prose ends up shorter than it was or the same length. Status, evidence, proof and
verifiedAtdo not change on these three rows. The claim's surface names these rows, so this is not an extension. After round 2 comes the at-tier review, withneeds:contract-reviewon the PR and both cards.
Generated by Claude Code
- Three rows go
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20287, "folded_issues": [ 20299 ], "round": 2, "status": "done", "branch": "claude/issue-20287-20299-ledger-flips-at-pin", "pr": "https://github.com/objectstack-ai/objectstack/pull/20814", "head": "67b954acb8", "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (subagent; the parent seat's harness-stamped id)", "premise_still_valid": true, "summary": "Round 2 per ruling 5907340127. Two commits were appended on top of 41b5470514 with no force-push. 1912c355b6 is the main merge, made with scripts/pm/os-regen-merge.sh: main 157baa75f2, no conflict, no os-regen pending, and the branch's two state-count shards kept. 67b954acb8 rewrites the notes of the 3 rows that stay dead, and nothing else changes. Proof: a parsed JSON comparison of permission.json and view.json against HEAD~1, with those three notes removed, is identical. status, evidence, proof, verifiedAt, producer and evidenceScope are untouched, and no other row moves. No tracker number is added; the old notes' #7427 and #7131 are dropped. Each note is shorter: 1643 to 939, 312 to 270 and 2083 to 820 characters, 4038 to 2029 in total. Each note states only what holds at the pin db11afd4967 and cites the checked symbols. RLS: PermissionPreview draws label and description, but permission items go to PermissionMatrixEditPage, so no route mounts the preview and there is no reader. view.label: the container never enters a Studio enumeration, so it never reaches the preview; the one-entry tab strip is not the obstacle. state-counts and README did not change (check:liveness reports the shards current). The PR body is untouched: the read-back is byte-identical to the round-1 body. The worktree was rebuilt from the remote branch at 41b5470514. Since the merge, main has moved 4 commits to 810d42b69c: driver-sql, half-state-patrol CI, a query doc and a skills doc. None of them touches a branch path, so there was no second merge. The merge queue rebuilds on current main.", "notes_before_after": [ { "row": "permission.rowLevelSecurity.label", "chars_before": 1643, "chars_after": 939, "before": "CORRECTED 2026-07-30 (was live with no evidence): no consumer in either repo. VERDICT RE-TESTED AND UPHELD 2026-08-10 (#7427) against the maintainer ruling that a designer preview rendering a key to a human is a runtime consumer (2026-08-10, #7131; README, 'Designer previews count as consumers'). This row is the closest structural twin of the four rows that ruling re-graded — a display `label` marked dead — so it was measured rather than assumed, and it comes out the other way. THE MEASUREMENT, at objectui @e9ab52f9: PermissionPreview IS registered for `permission` (previews/index.ts:71) and IS reachable (ResourceEditPage.tsx:949), so the preview lookup runs; but PermissionPreview.tsx:111 reads `rowLevelSecurity` only as an ARRAY and PermissionPreview.tsx:164 renders `${rls.length} RLS rules` — a COUNT. It never indexes a policy, never reads `.label`, and no policy field reaches a human through it. The 2026-07-30 wording 'PermissionPreview counts them' was exact, and counting is not rendering: the ruling turns on the VALUE being shown to a person, which is precisely what a length does not do. The other measured surface is PermissionAdvancedFacets.tsx:192-193 (reads `draft.rowLevelSecurity`, strips retired keys) and :264 (writes it back) — an authoring FORM, the 'authoring surface echoing input' the 2026-07 correction rejected, and the new ruling names previews, not edit forms. So both halves of the original closure survive it. Benign display metadata — deliberately NOT authorWarn'd. To re-open this row, the thing to look for is a preview that renders the policy's label text, not another surface that counts policies.", "after": "No reader reaches it, measured at objectui @db11afd4967. PermissionPreview.tsx#readPolicies reads each policy's `label`, and `PermissionPreview` draws it in its Row-Level Security list, but no route mounts that preview for `permission`. ResourceEditPage.tsx#MetadataResourceEditPage hands every non-create `permission` item to the custom EditPage that services/builtinComponents.tsx registers, `PermissionMatrixEditPage`. That page renders no preview, and its RLS form `PermissionAdvancedFacets` reads no policy label. Create mode previews only object, report and dataset. The other `getMetadataPreview` callers (`EmbeddedItemEditor`, `StudioDesignSurface`, the dev-only preview gallery) never open a `permission`. A preview no route mounts is a read point that never runs (README, 'Designer previews count as consumers'). Benign display metadata, deliberately NOT authorWarn'd. To re-open: a mounted surface that draws the policy's label." }, { "row": "permission.rowLevelSecurity.description", "chars_before": 312, "chars_after": 270, "before": "CORRECTED 2026-07-30 (was live with no evidence): same closure as label. RE-TESTED AND UPHELD 2026-08-10 (#7427) with `label`, same measurement at objectui @e9ab52f9 — the permission preview counts RLS policies (PermissionPreview.tsx:164) and renders no field of any individual policy. Benign — not authorWarn'd.", "after": "Same closure as `label`, at objectui @db11afd4967: `PermissionPreview` draws each policy's `description` beneath its row, but no route mounts it for `permission`. `PermissionMatrixEditPage` takes the item, and its RLS form reads no description. Benign, not authorWarn'd." }, { "row": "view.label", "chars_before": 2083, "chars_after": 820, "before": "Display metadata on the container with no runtime consumer. VERDICT RE-TESTED AND UPHELD 2026-08-10 (#7427) under the maintainer ruling that a designer preview rendering a key to a human is a runtime consumer (2026-08-10, #7131; README, 'Designer previews count as consumers'). It is the twin of `translation.label`, which that ruling DID re-grade, so it was measured, and it splits on REACHABILITY — the half of the README rule that is easy to skip. THE MEASUREMENT, at objectui @e9ab52f9: ViewPreview is registered for `view` (previews/index.ts:37) and resolved by ResourceEditPage.tsx:949, and it genuinely READS this key — ViewPreview.tsx:115 injects `(body).label ?? (draft).label ?? name` as the label of the single named listView it hands to the `object-view` schema. The read is real; the RENDER is not reachable. That injected label has exactly two render paths in plugin-view's ObjectView, and this preview takes neither: ObjectView.tsx:1119 renders `{view.label || key}` as a named-view tab, but ObjectView.tsx:1112 returns null when the map holds one entry or fewer, and ViewPreview always injects exactly one; ObjectView.tsx:993 passes the label into the `renderListView` list-view schema (ListView appends it to export filenames), but `renderListView` is a prop and plugin-view/src/index.tsx:58-64 registers the bare `ObjectViewRenderer` for `object-view` — it renders ObjectView with schema and dataSource only, so the prop is undefined on this path. A read point whose render cannot fire is what PR #7425 meant by 'a preview no registry hands a draft to is a read point that never runs', one level in: here the registry does hand it a draft, and the draft's label still reaches no human. Not authorWarn'd: the README's rule is that pure display annotation (description, tags, icon) must not warn, and the job.label / translation.label precedent is this exact case — docs-shaped, deliberately kept, ADR-0033 exempt from enforce-or-remove. To re-open: a second listView entry, or `renderListView` wired on this path, would make it render — either change flips this row.", "after": "Display metadata on the `defineView` container. No reader reaches it, measured at objectui @db11afd4967. ViewPreview.tsx#ViewPreview draws `draft.label` as the preview heading (`ViewLabelHeading`), but the Studio draft is always a ViewItem, never the container. `getMetaItems` drops aggregated containers from every `view` list, and Studio's view `listFilter` (services/builtinComponents.tsx) drops them again. `expandViewContainerWithDiagnostics` gives each ViewItem the `label` of its list or form entry, never the container's. So no Studio list or quick-find hands the container to the preview, and its own label reaches no human there. The `name` row draws the same container/ViewItem line. Not authorWarn'd: docs-shaped, deliberately kept (ADR-0033). To re-open: a Studio surface that draws a container's own label." } ], "tests": "At 67b954acb8. Through os-verify-lock: pnpm check:liveness (packages/spec) 'VERDICT command-exit 0'. Per-type counts are unchanged from round 1: permission 36 live, 6 dead; view 78 live, 11 dead; app 50 live, 8 dead, 1 planned; connector 31 live, 1 planned, 23 dead. Symbol anchors 830/830. Key-mention: 662 asked, 661 anchored, 1 exempt. state-counts current. Shard sum: 960 live, 122 dead. vitest --project local scripts/liveness: Test Files 10 passed, Tests 263 passed. vitest --project repo count-shards-merge, 3 connector retirement tests, rls-tags-retirement and view-list-tabs-retirement: Test Files 6 passed, Tests 106 passed. vitest --project local view-field-order-composition.pin and aria-carrier-tombstones: Test Files 2 passed, Tests 31 passed. spec build then check:generated in one locked command: 'All 15 generated artifacts are up to date', VERDICT command-exit 0. dispatch-gates --commands derived the same 60 families as round 1. Change set: 8 paths vs merge base 157baa75f. All 60 were run. The check:* gates ran outside the lock, per the os-dev.md rule that check:* gates do not take the lock. The seat asked for the lock, so this conflict is flagged. dispatch-gates --ran with exit codes, under the lock: '60 derived famil(ies) accounted for — 58 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)', VERDICT command-exit 0. Of the 60: 57 exit 0. 1 exit 1: check:platform-checklist, the same pre-existing main red on identity-auth.json auth-plugin.ts#twoFactor; inputs untouched, not per-PR CI. 2 exit 3, NOT MEASURED: check:dual-build-cjs-loads and check:lean-entry-closure, PREREQUISITE NOT MET because they need whole-workspace dist. That reconciliation also printed a STALE TREE warning. The two stale files are .github/actions/half-state-patrol/action.yml and .github/workflows/half-state-patrol.yml, and neither is on this diff's path. CI at 67b954acb8, deduped by name: 35 check-runs. 32 success. 3 skipped: Build Docs (ci.yml filter docs=false), Console Pin Gate (filter console=false; .objectui-sha unchanged) and Packed-tarball smoke (opt-in; label needs:pack-smoke absent). All 7 required contexts are success: Lint & Repo Gates, TypeScript Type Check, Test Core (1-6), Dogfood Regression Gate (1-3), Build Core, Temporal Conformance, Governed Surface Queue Guard.", "mcp_calls": "0", "api_writes": "1 REST write this round: post-stamped POST /repos/objectstack-ai/objectstack/issues/20287/comments (this report), sent through the fleet-write relay as one POST /repos/objectstack-ai/objectstack/dispatches. Also 1 git push, not REST: 41b5470514..67b954acb8, fast-forward. No PR body, label or assignee write.", "open_questions": [], "out_of_scope_findings": [ "carrier: platform-checklist-watchdog.yml · noted, not filed (unchanged from round 1): check:platform-checklist red on main at identity-auth.json auth-plugin.ts#twoFactor ABSENT SYMBOL." ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsLanded: PR #20814 →
15b586dcff; B2 is done and the card closes · 2026-09-30T10:16Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5906102916.- Landed: PR fix(spec): grade connector.actions.description/outputSchema and app.areas.description live at the objectui pin #20814 merged through the merge queue at 2026-09-30T10:13Z as
15b586dcff, with one parent,4b45afaed5. - Content check: each of the 8 files has the same
git patch-id --stableas the reviewed head67b954acb8against its merge base157baa75f2. - Review: the at-tier PASS record
5908669059was taken at67b954acb8, which is the head that landed. - B2 delivered:
connector.actions.descriptionandconnector.actions.outputSchemaareliveinpackages/spec/liveness/connector.json, each citing its flow-designer reader and producer at the.objectui-shapindb11afd4967c. - Why the seat closes the card: the PR's
Closes #20287did not close it through the merge queue. Nothing else was open here. - The fold's other half: studio: show the authored
label/descriptionon flows, hooks, app areas, RLS policies and the view container (7 keys) #20299 keeps three rows, and its own landing record says where they go.
Generated by Claude Code
- Landed: PR fix(spec): grade connector.actions.description/outputSchema and app.areas.description live at the objectui pin #20814 merged through the merge queue at 2026-09-30T10:13Z as
Ruled: 5881831013 · letter B (connector triggers family retired now; ADR-0041 Tier 3 stands; action half per the seat) · 2026-09-29T01:26Z
Filing gate: ① a declared≠enforced family, filed as one sweep card per family under ruling A′ item ④ on #18900 (
5727134555). This is triage's standing request5857165909on the seat post. Familyconnector-flow-surface, seat verdict ENFORCE.reach:the declared authoring door.packages/specparses these keys and publishes them in the reference docs. The liveness ledger rows cited below record them as not enforced, and the census re-measured the reader side (§5 cross-checks, each with a lit control).Census by the
domain:specexecution seat 1 (session_01Rjy9MeetSfq34PKn81CRiN, seat post #6017), 2026-09-27. Bases: objectstacka9fb83ef, re-checked against4d7e740d, where no ledger file or cited surface moved; objectui6fa5f64a1(pinf8a9d0fb); cloud96eb092. Ledger instrument:check-liveness.mts --json, whosebyStatusequals the committedstate-counts.mdrow for row. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. The ranking is by value, user-visible risk × keys. This family's rank is5of 16. The sibling family cards filed so far are #20273, #20274, #20281 and #20282.Capability: Connector triggers (polling / webhook) that start automations; connector action descriptions shown in the designer; typed action outputs usable downstream
connector.actions.descriptionpackages/spec/liveness/connector.json:87engine.ts#getConnectorDescriptorscopiesdescription: a.descriptioninto theGET /api/v1/automation/connectorspayload, and no consumer in either repo reads it back: objectui's three connector-descriptor con…connector.actions.outputSchemapackages/spec/liveness/connector.json:100inputSchema, projected the same way and consumed by nothing — which is exactly what makes this row falsifiable rather than a guess.engine.ts#getConnectorDescriptorspublishesoutputSchema: a.outputSchema; the census over both repos finds …connector.triggers.keypackages/spec/liveness/connector.json:116ConnectorTriggerSchema's docblock reads 'connector.triggers.labelpackages/spec/liveness/connector.json:121triggers.key— nothing reads a connector trigger.connector.triggers.descriptionpackages/spec/liveness/connector.json:126triggers.key.connector.triggers.typepackages/spec/liveness/connector.json:131triggers.key.pollingandwebhookname two engines that do not exist for connectors: no polling loop readsintervalSeconds, and the webhook dispatcher is driven bysys_webhookrows materialized from the TOP-LEVE…connector.triggers.intervalSecondspackages/spec/liveness/connector.json:136triggers.key. Renamed fromintervalby the protocol-18 conversion (#15680/#14478) so the unit lives in the key name — a rename that made the declaration honest without making it enforced.Mainstream evidence:
x-ms-trigger); actionsummary/descriptionshown in the designer; response schema becomes "dynamic content" tokens.Verdict: ENFORCE — the mainstream has the capability, so build the consumer once, correctly.
Reader that must exist / disposition: objectstack packages/services/service-automation/src/engine.ts#registerConnector (walks
parsed.actionsonly, :3698) must registertriggersinto the flow trigger registry with a polling loop / webhook receiver;#getConnectorDescriptorsalready projects description/outputSchema. objectui packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx#connectorActionsToOptions (:342, reads key+label only) must showdescription, and flow output refs must be typed fromoutputSchema.User-visible risk (2): The schema docblock itself says triggers are NOT YET ENFORCED (#3197), yet a descriptor that authors them parses clean.
outputSchemais published on the wire and ignored.Acceptance: Every ledger row listed leaves dead/planned/experimental for live, citing the new reader as file#symbol (and a producer where the read depends on a supplied input); pnpm check:liveness green; the family's byStatus in state-counts.md regenerated.
Lane: domain:spec parent; objectstack domain:services sub-issue (triggers, descriptors) + objectui sub-issue (designer), per the A′ seam rule
File surface: packages/spec/src/integration/connector.zod.ts:940-941 · packages/services/service-automation/src/engine.ts (registerConnector, getConnectorDescriptors, trigger registry) · objectui packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx
Dedupe:
ConnectorTrigger \| connector.{0,40}triggers?\b \| triggers\.intervalSeconds→ 3 open hits. None carries a key of this family:triggers.interval,circuitBreaker.monitoringWindow, cube sub-day granularities); does not carry any open keyconnectors/triggersDedupe:
actions\.outputSchema \| actions\.description \| getConnectorDescriptors \| connectorActionsToOptions \| nodeOutputRefs→ 0 open hits.四轴: