Skip to content

feat(runtime)!: the flow clone door lands the copy switched off in a writable package, by default the paired <base>.custom, never package-less - #28621

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-sdqbrt
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-sdqbrt

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Requested by Leehom · project thread

Fixes #28149

Clause-②: yes (narrowing) — POST /api/v1/automation/:name/clone accepts a new optional package member and answers a new packageId member, and a request it accepted before is now refused: a clone of a flow no read-only package ships, sent with no package, is refused 400 where it was written package-less.
Metadata-protocol: added — CloneFlowRequestSchema and CloneFlowResponseSchema are new authorable API schemas in @objectstack/spec/api (the authorable-surface/api.json shard gains them), and the request's package member is a new key a caller can write. No existing key is removed, refused or changed in meaning.
Skills-docs: none — no page under skills/** describes the clone door. The one docs sentence that does (content/docs/concepts/metadata-lifecycle.mdx, the ADR-0126 sanctioned-path table) is updated in this diff: it says where the copy lands, that it is switched off, and when package is needed.

BREAKING request narrowing at the flow clone door (@objectstack/runtime's /automation domain), per ADR-0146 D3/D4. The claim declared yes (widening) and asked for the narrowing arm if measured so: a request that answered 200 before is refused now, so the one arm is (narrowing).

维护者速读

  • 改了什么:流程克隆门(POST /automation/:name/clone)改为"新建相似":副本写进可写包,默认是源包的配对定制包 <base>.custom(只经 N-13 的 ensure 门创建),也可用可选的 package 指定;副本写成关闭状态(status: 'obsolete'),响应用 packageId 说明落在哪个包;Setup 的"在 Studio 打开克隆"链接按这个包打开。
  • 为什么改:ADR-0146 D3/D4 与 ADR-0070 D5/D6:克隆以前写成无包环境行,且 draft 在引擎里和 active 一样会运行,副本与原流程同时触发。
  • 风险与回滚:未由只读包发布的流程(环境流程)不带 package 克隆现在返回 400;读取副本 status 为 draft 的调用方会看到 obsolete。纯 revert 即可回滚,没有存量数据迁移。
  • 先看:packages/runtime/src/domains/automation.ts 的 resolveCloneTarget,以及共享目标规则 packages/runtime/src/domains/new-similar-target.ts(权限集克隆 runtime: the permission-set clone door lands "new similar" in a writable target package, by default the source package's <base>.custom, never package-less #28563 会复用同一文件)。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run --project local src/domains/automation-flow-clone-custom-package.test.ts(真实协议、注册表与 sqlite 存储,13 例)。
  • 需要你做的:席位按合约层级做 contract review 后落地;PR 正文里 "draft vs obsolete" 与拒绝码的两处取舍请看一眼。

What changes

  • Spec. CloneFlowRequestSchema ({ name, label, package? }, closed) and CloneFlowResponseSchema (data: { flow, packageId, notice }) in @objectstack/spec/api, with CloneFlowRequest/CloneFlowRequestParsed/CloneFlowResponse/CloneFlowResponseParsed, added to AutomationApiContracts. Generated surfaces regenerated (api-surface, export-origins, declaration-map, authorable-surface, json-schema manifest, strictness-ledger counts, reference docs); OpenAPI regenerates from the same schemas at build.
  • Door. The clone arm reads its key set off the schema. After the existing gates and the taken-name check it resolves the target:
    • package given → that package;
    • omitted → protocol.packagedArtifactOwner({type:'flow', name}) (the loader's fact, never the engine's _packageId stamp) names the base, and protocol.ensureCustomizationPackage({ basePackageId }) returns the pair. That is the body of POST /packages/:id/ensure-customization, called in process, not re-implemented;
    • omitted and no package ships the source → 400 VALIDATION_FAILED located on package (code: 'required').
  • Target rule (domains/new-similar-target.ts, shared with the permission-set clone door): not installed → 422 WRITABLE_PACKAGE_REQUIRED; read-only (isWritablePackage) → 422 WRITABLE_PACKAGE_REQUIRED; writable but its dependency closure (resolvePackageClosure, the save door's own walk) does not hold the source's package → 409 RESOURCE_CONFLICT. Each answer carries details.packageId and details.docs (ADR-0146), and nothing is written.
  • Write. The copy is saved with packageId: <target> and parentVersion: null (a first write), so a name the target already holds answers the clone's taken-name 409 instead of overwriting the row. FLOW_CLONE_STATUS is 'obsolete', and the notice says the copy is switched off.
  • SDK. client.automation.clone(name, body); the route-ledger row moves from server-only to sdk with both schemas named.
  • Setup rider. PackagedAutomationPage.tsx reads data.packageId and links to artifactStudioPath(packageId, { type: 'flow', name }); no link when the answer names no package.
  • Docs. metadata-lifecycle.mdx's flow row states the landing rules.

Two judgement calls, for the review

  1. "Inactive" is status: 'obsolete', not draft. The card says "draft", but the engine disables a flow only on obsolete/invalid, and draft arms exactly like active (the pre-change notice already said "status: 'draft' is not an off-switch"). obsolete is the value Studio's Enabled switch writes for Off and the one Studio's new-flow skeleton uses. The red-first run read 'draft' on the old door; the ablation that puts 'draft' back reds 4 cases.
  2. Refusal codes. DUPLICATE_SOURCE_NOT_A_BASE names a bad SOURCE, and every refusal here is about the TARGET, so it is not reused. Unknown and read-only targets take the save door's own WRITABLE_PACKAGE_REQUIRED; out-of-closure takes 409 RESOURCE_CONFLICT, as the ensure door answers a base whose declared state rules its pair out.

Also: if the ensure call succeeds and the save is then refused, the pair stays. That is the ensure door's idempotent result, and the next clone reuses it.

Tests

  • New automation-flow-clone-custom-package.test.ts: real ObjectQL + sqlite sys_metadata + the real protocol and its ensure door, packages base (system scope), dependent writable, unrelated writable, sealed. It covers: default lands in the pair (one row, bound to the pair, answer parsed by CloneFlowResponseSchema); obsolete stored, registered and answered; no base envelope; the original untouched; a second clone reuses the pair; an explicit dependent target; refusals (sealed 422, unknown 422, unrelated 409, the base itself 422) with nothing written; blank package 400; a local source without package 400 required; a local source into a named package; and the route-ledger row (disposition, client, schemas, key set, an undeclared key refused). Red first on the base sources: 11 red, 2 green.
  • Updated suites that now land in a package: automation-flow-clone, -credential, -door-package-address, automation-packaged-base-lock, automation-tenant-authored-write, automation-flow-save-actor; dogfood automation-flow-clone-door expects obsolete and com.example.crm.custom; the Setup page test pins the link to the answer's package, plus a no-link control.
  • Ablation (each leg committed first, restored by the tool, HEAD blob verified): ensure call replaced by target = owner → 5 red; FLOW_CLONE_STATUS = 'draft' → 4 red.

Verification

At head c3369fd2f5 (main merged at d94dd3a65):

  • @objectstack/runtime local + repo projects: 505/505 files, 9108 passed / 19 skipped (os-verify-lock VERDICT command-exit 0).
  • @objectstack/client: tsc -p tsconfig.test.json clean, 53/53 files, 690 tests.
  • Setup page test: 32/32. @object-ui/app-shell type-check could not resolve its sibling @object-ui/* packages in this container (no ui build), so it is left to CI's Type Check UI; no error names the two touched files.
  • Gate union from dispatch-gates --commands (121 gates): the first pass flagged stale generated spec artifacts, a missing ...Parsed alias, an un-aliased @objectstack/lint/runtime import in runtime tests, an issue id in a route-ledger note, and the new domain file missing from check-route-envelope. All fixed in this head. Build-dependent gates and the full dogfood battery are re-run at this head, and their readings are in the os-dev-report on the card.
  • Scoped eslint over the touched files: 0 errors (one existing set-state-in-effect warning in PackagedAutomationPage.tsx, not on a changed line).

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5pRVPsxnFxPBAQLUMh

…tom, switched off (WIP)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pRVPsxnFxPBAQLUMh
…s land in a package

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pRVPsxnFxPBAQLUMh
…tup link, docs, changeset

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pRVPsxnFxPBAQLUMh
…declare the target helper to route-envelope

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pRVPsxnFxPBAQLUMh
…jectstack/lint/runtime to source in runtime tests

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pRVPsxnFxPBAQLUMh
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/client, @objectstack/runtime, @objectstack/spec, touching 38 documentable anchor(s). ⚠️ 6 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

24 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 6e11a929e453e1f9a78a9eb7b311c46b2fa8d094.

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

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: responseSchema (6 routes)
  • 2 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 177 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 6e11a929e453e1f9a78a9eb7b311c46b2fa8d094 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 6e11a929e453e1f9a78a9eb7b311c46b2fa8d094

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 89a32773436773433ea43b40f8e6c13151df276f
Local-runs: none
Scope: packages/spec/src/api/automation-api.zod.ts + packages/runtime/src/domains/automation.ts, packages/runtime/src/domains/new-similar-target.ts, packages/runtime/src/flow-clone.ts, packages/runtime/src/route-ledger.ts, packages/runtime/src/dispatcher-plugin.ts, packages/client/src/index.ts, packages/metadata-protocol/src/protocol.ts (packagedArtifactOwner), packages/metadata-protocol/src/protocol-customization-package.ts (ensureCustomizationPackage), packages/types/src/validation-failure.ts, scripts/check-route-envelope.mjs, packages/runtime/package.json, packages/runtime/vitest.config.ts, pnpm-lock.yaml, packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, packages/spec/dropped-refinements.baseline.json, packages/spec/export-origins/api.json, packages/spec/json-schema.manifest/api.json, packages/runtime/src/domains/automation-flow-clone-custom-package.test.ts, packages/runtime/src/domains/automation-flow-clone.test.ts, packages/runtime/src/domains/automation-flow-clone-credential.test.ts, packages/runtime/src/domains/automation-flow-door-package-address.test.ts, packages/runtime/src/domains/automation-flow-save-actor.test.ts, packages/runtime/src/domains/automation-packaged-base-lock.test.ts, packages/runtime/src/domains/automation-tenant-authored-write.test.ts, packages/qa/dogfood/test/automation-flow-clone-door.dogfood.test.ts, packages/qa/dogfood/test/flow-credential-channel.dogfood.test.ts, packages/qa/dogfood/test/flow-provenance-server-held.dogfood.test.ts, ui/packages/app-shell/src/views/setup/PackagedAutomationPage.tsx, ui/packages/app-shell/src/views/setup/PackagedAutomationPage.test.tsx, docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md

Faces owed and served in this one record: the schema face (packages/spec/src/api/automation-api.zod.ts is a published schema; hits = that path) and the narrowing face (Clause-②: yes (narrowing) at the head of the PR body and the changeset; its floor is every diff path outside the prose faces, listed above). The declaration face is subsumed. Read-only shape held: the diff from merge-base de3fafcb824b0c88daedc01e8ba67eb918862bbc, the card, the order, the dev report (comment 6556433091), the head's check-runs over REST, and a throw-away worktree merge of the head onto origin/main that was discarded. Head 89a32773436773433ea43b40f8e6c13151df276f differs from the dev report's 71dca746942e577463d6c7a48a689898b47c5bed in exactly one file, .changeset/28149-flow-clone-lands-in-package.md (three lines); every other finding below was read at the earlier head and holds byte-identically at this one.

① Derived judgments

The accept set and public face the diff moves, each judged:

  1. New request schema CloneFlowRequestSchema (@objectstack/spec/api): z.object({ name, label, package? }).strict(), name/label string().min(1), package string().min(1).optional(), each with a describe naming the refusal it draws (409 taken name; 422 WRITABLE_PACKAGE_REQUIRED / 409 RESOURCE_CONFLICT on a bad target; 400 VALIDATION_FAILED on package when omitted for a source no read-only package ships). Added to AutomationApiContracts.cloneFlow with method and path. Correct. The describe text matches the door's actual answers (read against resolveCloneTarget, newSimilarTargetRefusal and validationFailure in @objectstack/types, which carries code: 'VALIDATION_FAILED', status 400).
  2. The door honours the schema's member set. handleAutomationRequest's clone arm now derives declaredKeys from CloneFlowRequestSchema.shape and refuses any other key 400 unknown_field on that key, so door and declaration cannot disagree on the closed set; the contract suite's "an undeclared one is refused on its key" case pins it. The door does not safeParse the body with the schema; it hand-validates the three members. The one residual: the door refuses a whitespace-only package (trim() === '', 400 invalid_type) where min(1) would accept it. That is the door being one notch stricter than its declaration on a value no caller means; noted, not a defect in the accept set a consumer relies on.
  3. New response schema CloneFlowResponseSchema = BaseResponseSchema.extend({ data: { flow: FlowSchema, packageId: string, notice: string } }). The whole envelope is named, as the uninstall row's is. The door answers deps.success({ flow: servedFlowDefinition(clone), packageId: target.packageId, notice: FLOW_CLONE_NOTICE }); the new suite parses every 200 through this schema. Correct. packageId is the new member; flow and notice are unchanged. There was no response schema before (the row was server-only); it now exists and the ledger row names both schemas.
  4. Accept-to-refuse moves, exactly: (a) a clone of a source that no read-only package ships (an environment flow, or one stored in a writable package), sent without package: 200 package-less before, now 400 VALIDATION_FAILED with fields: [{ field: 'package', code: 'required' }], nothing registered or stored. (b) a clone of a packaged source whose base cannot be paired (the ensure door's own 404/409 refusals, relayed as thrown, e.g. a base with no namespace) — before, package-less 200. (c) nothing else moves from accept to refuse: the taken-name 409, credential 409, walled refusal, manage_metadata 403, anonymous 401 and unknown-source 404 are untouched, and a packaged source with no package still answers 200 (now landing in the pair). The refusal families are existing ones only: VALIDATION_FAILED (the validation-failure kit), WRITABLE_PACKAGE_REQUIRED (the save door's own, uninstalledNamedBaseRefusal / isWritablePackage) and RESOURCE_CONFLICT. DUPLICATE_SOURCE_NOT_A_BASE is correctly not reused: the order asked for its family "unless the code's meaning doesn't fit (then say why)"; it names a bad SOURCE and every refusal here is about the TARGET; the dev said so on the PR and kept that family's shape ([tag] sentence, details.packageId, details.docs). No new code is minted. Correct.
  5. Default target. package omitted → protocol.packagedArtifactOwner({ type: 'flow', name }) (the loader's set, never the engine's _packageId stamp) names the base → protocol.ensureCustomizationPackage({ basePackageId }) returns the pair → the pair is then judged by the same target rule. Read in protocol-customization-package.ts: the ensure body is idempotent (an existing pair is returned unchanged or refused naming what it lacks, never adopted or rewritten), per-pair in-flight calls are serialised, a writable base is refused 409, an unknown base 404. In-process, the one creation path, idempotent reuse — pinned by "a second clone reuses the pair" (one installed pair, no second persist). Correct.
  6. Explicit target. newSimilarTargetRefusal: not installed → 422 WRITABLE_PACKAGE_REQUIRED; !isWritablePackage → 422; writable but resolvePackageClosure(target).dependencies lacks the source's package → 409 RESOURCE_CONFLICT. The closure walk is @objectstack/lint/runtime's, the same one protocol-write-package-scope.ts (the save door's publish gate) uses; @objectstack/lint does not depend on @objectstack/runtime, so the new workspace:* dependency in packages/runtime/package.json introduces no cycle (the lockfile hunk is the matching link). Refusals are answered before anything is registered; the table case pins registerFlow not called, no row, no pair minted. Correct.
  7. Copy inactive, original unchanged, no double-run. FLOW_CLONE_STATUS moves 'draft' → 'obsolete'; the engine disables only on obsolete/invalid (its registerFlow), so draft armed exactly like active — the measured double-run on showcase. Pinned: the answer, the stored row and the registered flow all read obsolete; the source stays active with no row written under its name. The card's literal word was "draft"; the card's intent ("so a clone never double-runs beside its original") is only met by obsolete, and the dev stated the deviation on the PR. Judged correct; the seat may wish to amend the card's wording when closing it.
  8. No base envelope on the copy. cloneFlowDefinition still drops _packageId, _packageVersion, _provenance, _lock; the save carries packageId: target as a save parameter (the row's package_id binding), not as a stamp on the item. Pinned: the answer has none of the four keys; the row's item _packageId is not the base and _provenance is not 'package'. The dogfood provenance fixture's flipped assertion (isCodeArtifactBody(served.doc) now true) is the /meta read surfacing the row's binding as _packageId: pair — the same served shape the file already measures for META_BOUND, and the fixture now also proves the copy's round-trip PUT into the pair saves 200. That is the pair's binding, not the base's envelope. Correct.
  9. First-write pin. The clone's saveMetaItem carries parentVersion: null; a 409 METADATA_CONFLICT from the store is translated by flowWriteRefusal into the clone door's own taken-name 409 with its remedy. A row the target already holds under the name (one the engine's collision probe cannot see) is never overwritten. Correct, and a sound tightening.
  10. Route ledger row: server-only → sdk, client: 'automation.clone', requestSchema: 'CloneFlowRequestSchema', responseSchema: 'CloneFlowResponseSchema'; the note describes the door as it now is. Mounted: dispatcher-plugin.ts line 1878 still mounts POST {base}/automation/:name/clone and dispatches to the domain — the mount predates this PR and is unchanged, so this PR does not repeat main red: Dogfood Regression Gate — GET /packages/:id/customizations is ledgered but not mounted (#28595), so the route 404s at runtime #28622's ledgered-but-unmounted defect (that defect, GET /packages/:id/customizations, is main's from feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595 and is reported by the dev under out_of_scope_findings, not touched here). Correct.
  11. SDK client.automation.clone(name, body: CloneFlowRequest), resolving to the data member of CloneFlowResponse — POST {route}/{encodeURIComponent(name)}/clone, JSON body, unwrapResponse. Types imported from @objectstack/spec/api. Correct.
  12. Generated artifacts match a regeneration of the schema: api-surface gains the six exports (schemas, types, ...Parsed); export-origins the same six with their source; declaration-map the four non-Parsed names (the existing pattern, cf. CloneDataResponse); json-schema.manifest the two schema ids; authorable-surface the three request members and the four envelope members; dropped-refinements.baseline the three FlowSchema refinement sites under api/CloneFlowResponse (the same sites CreateFlowRequest carries); reference docs count 1582 → 1584 and the automation-api page lists both; the strictness-ledger count 472 → 474. Consistent with each other and with the source; nothing hand-patched. Correct.
  13. Setup rider 6554259755. PackagedAutomationPage.tsx reads data.packageId into CloneResult.packageId, links artifactStudioPath(packageId, { type: 'flow', name }) and draws no link when the answer names no package; studioOrgScopePath import dropped. The view test pins the href starts with /studio/{encoded packageId}/automations, not /studio/~org, plus the no-link control. Correct.
  14. First-party callers. The only caller of this door in the monorepo is PackagedAutomationPage.tsx (the /home/claude/objectui copy is an archived mirror whose README points at objectstack/ui/, so it is not live); the SDK method is new. packages/metadata-protocol/src/packaged-base-regime.ts line 185 still prescribes body {name, label} in the regime's sanctioned-path text — still a valid body for the packaged base that refusal names (its copy lands in the pair), so not wrong; could mention package? (observation, no action owed). Fixture updates: flow-credential-channel case 6 cloned an environment flow package-less (200) and now creates a writable package and names it — exactly the declared narrowing, a legitimate fixture update, not a hidden caller; flow-provenance-server-held asserts the pair and the round trip (item 8). Neither hides a real caller breaking.
  15. Merge of main. The head's merge-base is de3fafcb824b0c88daedc01e8ba67eb918862bbc (post-feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595, merged 519089e171fcd49daeb1a83250c8ed6bb6000add). The PR's diff restricted to feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595's nineteen files touches only the shared registries (ledger, client, the spec shards, the docs index) with its own additive entries; feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595's GET /packages/:id/customizations row and ensure-customization mount are byte-preserved. A no-commit test merge of this head onto current origin/main (seven newer commits, none overlapping a PR path) applied cleanly and was discarded. Correct.

② Semver level

  • Clause-②: yes (narrowing) on the PR body and the changeset, line-initial, one arm. Per clause2-line.mjs that is the legal spelling for "widens one surface (optional package, packageId) and narrows another (package-less clone refused)"; (narrowing) is BREAKING, and the changeset carries the **BREAKING** banner and a feat(runtime)!: title. Consistent.
  • Metadata-protocol: added at this head, with @objectstack/spec: minor, @objectstack/runtime / @objectstack/client / @object-ui/app-shell patch. The gate (check-changeset-no-major.mjs) reads a moved packages/spec/authorable-surface/ shard as "a key added" and refuses none over it; the shard gains api/CloneFlowRequest:package and the other six members, so added is what the ratchet records and minor on the one package whose published source moved is the level the kind owes; added under Clause-②: yes is not a contradiction. Consistent; Check Changeset is green on this head.
  • At the dev-report head 71dca746942e577463d6c7a48a689898b47c5bed the lines read Metadata-protocol: none / spec: patch, which the gate refused (Check Changeset red, "Moved: packages/spec/authorable-surface/api.json"). That was a must-fix, found and discharged in the one-file delta to this head; nothing else moved.
  • ADR-0087 marker: exactly one, not-required (no-migration-prescription), with the reason (no existing key removed, renamed or re-shaped; no stored metadata changes; the remedy for the refused request is the request's own package). check-adr-0087-registration accepts it; judged honest — a copy written before this change stays where it was.
  • Check-runs at this head: Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Validate Package Dependencies, Dogfood Regression Gate, Docs links all green; Lint & Type Check and CI (Test Core, Type Check, Test UI shards) still in progress at review time. At the previous head the Test UI rollup read red only because its shards concluded cancelled (the rollup's own message: a shard killed by its timeout reads cancelled), with no failing test named. Landing still waits on a fully green head per the seat's ② rule; nothing in this diff is implicated.

③ Boundary flags

④ Design judgment

  • packages/spec/src/api/automation-api.zod.ts against the ruling and its first consumer. The ruling (epic [Epic] Package-level override: every customization of an installed package lives in its paired <base>.custom package, one declared override shared by code and Studio (supersedes #28130) #28236, Q6, "clone kept only as new similar, landing in the paired .custom"; ADR-0146 D3/D4; ADR-0070 D5/D6 retiring the package-less scope) is what the schema declares: a closed body, an optional package whose describe text states both the default (the pair, ensured on first use) and the one case with no default (a source no read-only package ships), and a response that names where the copy landed. Its first consumer is the door itself, which reads its member set, and the second is the Setup rider, which reads packageId; both are exercised by committed tests on the real protocol, registry and store (not mocks of the ensure door). The strict object is the right posture for a door whose body chose a destination: a misspelt package is refused on its key rather than landing the copy somewhere unnamed. The one design seam I would have liked tighter — the door parsing with the schema rather than mirroring its three checks by hand — is covered today by the shape-derived key set and the ledger-row test, so the two cannot drift in membership; it is a quality note, not a gap in the accepted contract.
  • The narrowing against the published spec on main: before this PR the spec published no request schema for the door (the ledger row was server-only, body spelled in prose as { name, label }), and the door wrote the copy to the scope ADR-0070 D5/D6 had already retired. The narrowing takes away a capability the platform's own ADRs had declared gone, and the spec's refusal (400 on package, code: 'required') is made real with a one-line remedy in the message. It does not strand a consumer: the only first-party caller clones packaged flows, which still answer 200. Holds.
  • Scenario check: red-first readings (11 red / 2 green on base sources: packageId undefined, draft not obsolete, package refused unknown_field, local clone 200 package-less) and the two ablations (ensure call bypassed → 5 red; status back to draft → 4 red) are stated in the dev report with the tool and restore verification; they match what the suite's cases would do and were not re-run here (read-only shape).

Implemented-by: claude/project-thread-sdqbrt
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

…opped-refinements header counts its new entry

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pRVPsxnFxPBAQLUMh
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pRVPsxnFxPBAQLUMh

# Conflicts:
#	packages/runtime/src/domains/new-similar-target.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 387e88ca7346efc317c9e5993b3ebe0b9170f375
Local-runs: none
Scope: packages/spec/src/api/automation-api.zod.ts + packages/runtime/src/domains/automation.ts, packages/runtime/src/flow-clone.ts, packages/runtime/src/route-ledger.ts, packages/runtime/src/dispatcher-plugin.ts, packages/runtime/src/domains/new-similar-target.ts (main's, read), packages/client/src/index.ts, packages/metadata-protocol/src/protocol.ts (packagedArtifactOwner), packages/metadata-protocol/src/protocol-customization-package.ts (ensureCustomizationPackage), packages/types/src/validation-failure.ts, packages/spec/src/api/automation-api.zod.test.ts, packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, packages/spec/dropped-refinements.baseline.json, packages/spec/export-origins/api.json, packages/spec/json-schema.manifest/api.json, packages/runtime/src/domains/automation-flow-clone-custom-package.test.ts, packages/runtime/src/domains/automation-flow-clone.test.ts, packages/runtime/src/domains/automation-flow-clone-credential.test.ts, packages/runtime/src/domains/automation-flow-door-package-address.test.ts, packages/runtime/src/domains/automation-flow-save-actor.test.ts, packages/runtime/src/domains/automation-packaged-base-lock.test.ts, packages/runtime/src/domains/automation-tenant-authored-write.test.ts, packages/qa/dogfood/test/automation-flow-clone-door.dogfood.test.ts, packages/qa/dogfood/test/flow-credential-channel.dogfood.test.ts, packages/qa/dogfood/test/flow-provenance-server-held.dogfood.test.ts, ui/packages/app-shell/src/views/setup/PackagedAutomationPage.tsx, ui/packages/app-shell/src/views/setup/PackagedAutomationPage.test.tsx, docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md

Faces owed and served in this one record: the schema face (packages/spec/src/api/automation-api.zod.ts is a published schema; hits = that path) and the narrowing face (Clause-②: yes (narrowing) at the head of the PR body and the changeset; its floor is every diff path outside the prose faces, all listed above). The declaration face is subsumed. Read-only shape held: the diff of this head against its merge-base, which is current origin/main 6e11a929e453e1f9a78a9eb7b311c46b2fa8d094; the card, the order, the dev report (comment 6556433091); the head's check-runs over REST; and the earlier reads of this PR at 89a32773436773433ea43b40f8e6c13151df276f, whose findings are re-stated here where the bytes are unchanged.

What moved since the previous record. This head is the merge of origin/main at 6e11a929 plus one commit, 0636ba2c186171dbc127cfc60706dea1370e220e. Main brought PR #28636 (#28563), which landed packages/runtime/src/domains/new-similar-target.ts as the superset (the objects limb), the @objectstack/lint dependency, the vitest alias, the check-route-envelope row and the lockfile link; the dev kept main's copy of each, so those five paths now diff zero against main and are no longer part of this PR's diff. The PR's diff against main is exactly 29 files: its own 27 as reviewed before, with packages/runtime/src/domains/automation.ts, flow-clone.ts, client/src/index.ts, automation-api.zod.ts, the Setup page and every test byte-identical to the previous head, plus two files from 0636ba2c: packages/spec/src/api/automation-api.zod.test.ts and packages/spec/dropped-refinements.baseline.json's header. The route-ledger and metadata-lifecycle hunks against main are byte-identical to the PR's original hunks (checked line by line), so the merge altered none of #28636's or #28586's text.

① Derived judgments

The accept set and public face the diff moves, each judged:

  1. New request schema CloneFlowRequestSchema (@objectstack/spec/api): z.object({ name, label, package? }).strict(), name/label string().min(1), package string().min(1).optional(), each with a describe naming the refusal it draws (409 taken name; 422 WRITABLE_PACKAGE_REQUIRED / 409 RESOURCE_CONFLICT on a bad target; 400 VALIDATION_FAILED on package when omitted for a source no read-only package ships). Added to AutomationApiContracts.cloneFlow with method and path. Correct. The describe text matches the door's actual answers (read against resolveCloneTarget, main's newSimilarTargetRefusal and validationFailure in @objectstack/types, which carries code: 'VALIDATION_FAILED', status 400).
  2. The door honours the schema's member set. The clone arm derives declaredKeys from CloneFlowRequestSchema.shape and refuses any other key 400 unknown_field on that key, so door and declaration cannot disagree on the closed set; the contract suite's "an undeclared one is refused on its key" case pins it. The door hand-validates the three members rather than safeParse-ing. One residual: it refuses a whitespace-only package (trim() === '', 400 invalid_type) where min(1) would accept it; the door is one notch stricter than its declaration on a value no caller means. Noted, not a defect in the accept set a consumer relies on.
  3. New response schema CloneFlowResponseSchema = BaseResponseSchema.extend({ data: { flow: FlowSchema, packageId: string, notice: string } }), the whole envelope named as the uninstall row's is. The door answers deps.success({ flow: servedFlowDefinition(clone), packageId: target.packageId, notice: FLOW_CLONE_NOTICE }); the new suite parses every 200 through this schema. Correct. packageId is the new member; there was no response schema before, and the ledger row now names both.
  4. Accept-to-refuse moves, exactly: (a) a clone of a source that no read-only package ships (an environment flow, or one stored in a writable package) sent without package: 200 package-less before, now 400 VALIDATION_FAILED with fields: [{ field: 'package', code: 'required' }], nothing registered or stored. (b) a clone of a packaged source whose base the ensure door cannot pair (its own 404/409, relayed as thrown): package-less 200 before. (c) nothing else: taken-name 409, credential 409, walled refusal, manage_metadata 403, anonymous 401 and unknown-source 404 are untouched; a packaged source with no package still answers 200, now into the pair. Refusal families are existing ones only — VALIDATION_FAILED, WRITABLE_PACKAGE_REQUIRED (the save door's own), RESOURCE_CONFLICT. DUPLICATE_SOURCE_NOT_A_BASE is correctly not reused: it names a bad SOURCE, every refusal here is about the TARGET, the dev said so on the PR and kept that family's shape ([tag] sentence, details.packageId, details.docs). No new code is minted. Correct.
  5. Default target. package omitted → protocol.packagedArtifactOwner({ type: 'flow', name }) (the loader's set, never the engine's _packageId stamp) names the base → protocol.ensureCustomizationPackage({ basePackageId }) returns the pair → the pair is judged by the same target rule. The ensure body (protocol-customization-package.ts) is idempotent — an existing pair is returned unchanged or refused naming what it lacks, never adopted or rewritten — serialises in-flight calls per pair, refuses a writable base 409 and an unknown base 404. In-process, the one creation path, idempotent reuse, pinned by "a second clone reuses the pair" (one installed pair, no second persist). Correct.
  6. Explicit target, through main's superset helper. resolveCloneTarget calls newSimilarTargetRefusal(ql, { target, subject, sourcePackageId? }) with no objects; on main the request type is { target; subject; sourcePackageId?; objects?: readonly string[] } and the objects branch runs only when objects is present and non-empty, so the flow door takes exactly the three checks it took from its own copy: not installed → 422 WRITABLE_PACKAGE_REQUIRED; !isWritablePackage → 422; writable but resolvePackageClosure(target).dependencies lacks the source's package → 409 RESOURCE_CONFLICT. newSimilarTargetRefusalAnswer is unchanged on main. The merge resolution is therefore sound: the flow door's behaviour under the superset is the behaviour the suite pins, and the objects limb is the permission-set door's alone. Refusals come before anything is registered; the table case pins registerFlow not called, no row, no pair minted. Correct.
  7. Copy inactive, original unchanged, no double-run. FLOW_CLONE_STATUS 'draft' → 'obsolete'; the engine disables only on obsolete/invalid, so draft armed like active (the measured double-run). Pinned: answer, stored row and registered flow read obsolete; the source stays active with no row under its name. The card's literal word was "draft"; its intent ("never double-runs beside its original") is met only by obsolete, and the dev stated the deviation. Judged correct; the seat may amend the card's wording on close.
  8. No base envelope on the copy. cloneFlowDefinition drops _packageId, _packageVersion, _provenance, _lock; the save carries packageId: target as a save parameter (the row's package_id), not as a stamp. Pinned: the answer has none of the four keys; the row's _packageId is not the base and _provenance is not 'package'. The dogfood provenance fixture's flipped isCodeArtifactBody is the /meta read surfacing the row's binding as _packageId: pair — the shape that file already measures for META_BOUND — and it now proves the copy's round-trip PUT into the pair saves 200. The pair's binding, not the base's envelope. Correct.
  9. First-write pin. The clone's save carries parentVersion: null; a 409 METADATA_CONFLICT from the store becomes the clone door's taken-name 409 with its remedy. A row the target already holds is never overwritten. Correct.
  10. Route ledger row: server-only → sdk, client: 'automation.clone', requestSchema/responseSchema named; the note describes the door as it now is. Mounted: dispatcher-plugin.ts still mounts POST {base}/automation/:name/clone and dispatches to the domain — the mount predates this PR and is unchanged, so this PR does not repeat main red: Dogfood Regression Gate — GET /packages/:id/customizations is ledgered but not mounted (#28595), so the route 404s at runtime #28622's ledgered-but-unmounted defect (which main has since fixed in fix(runtime): GET /packages/:id/customizations answers over HTTP, mounted beside the other package reads #28635). Correct.
  11. SDK client.automation.clone(name, body: CloneFlowRequest), resolving to the data member of CloneFlowResponse: POST {route}/{encodeURIComponent(name)}/clone, JSON body, unwrapResponse; types from @objectstack/spec/api. Correct.
  12. Generated artifacts match a regeneration: api-surface gains the six exports; export-origins the six with their source; declaration-map the four non-Parsed names (the existing pattern); json-schema.manifest the two ids; authorable-surface the three request members and four envelope members; reference docs 1582 → 1584 and the automation-api page lists both; the strictness-ledger count 472 → 474. The two 0636ba2c fixes: (a) dropped-refinements.baseline.json — its body gains api/CloneFlowResponse with the three FlowSchema refinement sites (the same sites CreateFlowRequest carries), and the hand-maintained header moves 246 → 247 schemas, 948 → 951 sites. I recounted the body at this head: 247 entries, 951 sites — the header now states the body, and the ledger grew by exactly the one schema this PR publishes, the growth the file's own description requires to be stated by hand. Legitimate, not a weakened pin. (b) automation-api.zod.test.ts — the contract-registry count moves 8 → 9 with a comment naming cloneFlow (Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149) and a new toHaveProperty('cloneFlow') assertion; no other assertion in that file moves. That is the pin catching the schema this PR adds and being told why, i.e. the registry test doing its job; the previous head would have been red there. Legitimate. Correct.
  13. Setup rider 6554259755. PackagedAutomationPage.tsx reads data.packageId, links artifactStudioPath(packageId, { type: 'flow', name }), draws no link when the answer names no package; studioOrgScopePath import dropped. The view test pins the href prefix /studio/{encoded packageId}/automations, not /studio/~org, plus the no-link control. Correct.
  14. First-party callers. The only caller in the monorepo is PackagedAutomationPage.tsx (the /home/claude/objectui copy is an archived mirror whose README points at objectstack/ui/); the SDK method is new. packages/metadata-protocol/src/packaged-base-regime.ts line 185 still prescribes body {name, label} — still a valid body for the packaged base its refusal names; could mention package? (observation, no action owed). Fixture updates: flow-credential-channel case 6 cloned an environment flow package-less (200) and now creates a writable package and names it — exactly the declared narrowing, legitimate; flow-provenance-server-held asserts the pair and the round trip (item 8). Neither hides a breaking caller.
  15. Merge of main. Merge-base is current origin/main 6e11a929 (post-feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595, post-feat(runtime)!: the permission-set clone door lands "new similar" in a writable target package, by default the source package's .custom (#28563) #28636); the diff against it is the PR's 29 files only, with no hunk of feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595's, feat(runtime)!: the permission-set clone door lands "new similar" in a writable target package, by default the source package's .custom (#28563) #28636's, docs(skills,docs): objectstack-platform and the metadata-lifecycle page teach the one effective read of a .custom declared override (ADR-0146 D8) #28586's or fix(runtime): GET /packages/:id/customizations answers over HTTP, mounted beside the other package reads #28635's altered (the shared helper, dependency, alias, envelope row and lockfile are main's bytes). Correct.

② Semver level

  • Clause-②: yes (narrowing) on the PR body and the changeset, line-initial, one arm — the legal spelling in clause2-line.mjs for "widens one surface (optional package, packageId) and narrows another (package-less clone refused)"; (narrowing) is BREAKING, the changeset carries the **BREAKING** banner and a feat(runtime)!: title. Consistent.
  • Metadata-protocol: added, with @objectstack/spec: minor and @objectstack/runtime / @objectstack/client / @object-ui/app-shell patch. check-changeset-no-major.mjs reads a moved packages/spec/authorable-surface/ shard as "a key added" and refuses none over it; the shard gains api/CloneFlowRequest:package and six more members, so added is what the ratchet records and minor on the one package whose published source moved is the level the kind owes; added under Clause-②: yes is no contradiction. Consistent. (The earlier head 71dca74694 declared none / patch and was refused by that gate; that must-fix was discharged at 89a3277343 and the changeset is unchanged since.)
  • ADR-0087 marker: exactly one, not-required (no-migration-prescription), with its reason (no existing key removed, renamed or re-shaped; no stored metadata changes; the remedy is the request's own package). check-adr-0087-registration accepted it at the previous head and the file is unchanged. Honest: a copy written before this change stays where it was.
  • Check-runs at this head: Governed Surface Queue Guard, PR-body guards, Spec property liveness, Dogfood Regression Gate, Check PR Size, Check Documentation Links green; Check Changeset, Lint, Type Check, Build and the Test shards still in progress at review time. Landing waits on a fully green head per the seat's ② rule; 0636ba2c is the dev's answer to the two spec reds the previous head's CI would have shown, and nothing else in this diff is implicated.

③ Boundary flags

④ Design judgment

  • packages/spec/src/api/automation-api.zod.ts against the ruling and its first consumer. The ruling (epic [Epic] Package-level override: every customization of an installed package lives in its paired <base>.custom package, one declared override shared by code and Studio (supersedes #28130) #28236, Q6: clone kept only as "new similar", landing in the paired .custom; ADR-0146 D3/D4; ADR-0070 D5/D6 retiring the package-less scope) is what the schema declares: a closed body, an optional package whose describe text states both the default (the pair, ensured on first use) and the one case with no default (a source no read-only package ships), and a response naming where the copy landed. Its first consumer is the door, which reads its member set; its second the Setup rider, which reads packageId; both are exercised by committed tests on the real protocol, registry and store, and now, with main's helper, on the exact target rule both "new similar" doors share. The strict object is the right posture for a body that chooses a destination: a misspelt package is refused on its key rather than landing the copy somewhere unnamed. The seam I would have liked tighter — the door parsing with the schema rather than mirroring its checks — is covered by the shape-derived key set and the ledger-row and contract-registry tests, so the two cannot drift in membership; a quality note, not a gap in the accepted contract.
  • The narrowing against the published spec on main: before this PR the spec published no request schema for the door (the row was server-only, body spelled in prose), and the door wrote the copy to a scope ADR-0070 D5/D6 had retired. The narrowing takes away a capability the platform's own ADRs had declared gone and makes the spec's refusal real (400 on package, code: 'required', with the remedy in the message). It strands no consumer: the only first-party caller clones packaged flows, which still answer 200. Holds.
  • Scenario check: the red-first readings (11 red / 2 green on base sources) and the two ablations (ensure call bypassed → 5 red; status back to draft → 4 red) are stated in the dev report with the tool and restore verification and match what the suite's cases would do; the two 0636ba2c fixes are the spec's own pins moving with the schema they count. Not re-run here (read-only shape).

Implemented-by: claude/project-thread-sdqbrt
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-01T11:14Z · head 387e88ca · #28149

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
    • A draft against main with no assignees and Fixes #28149. The changeset grades @objectstack/spec minor, carries a BREAKING banner and the ADR-0087 not-required marker. The title carries !.
    • check-governed-merges --pr 28621 reads NOT governed, 1141 changed lines (generated shards included). Landing tier S.
    • The claim (6554908823) names this PR's branch, claude/project-thread-sdqbrt.
  2. Against the order:
    • POST /automation/<name>/clone takes an optional target package under the new CloneFlowRequestSchema (strict). Its answer names the package the copy landed in (CloneFlowResponseSchema).
    • The copy is written switched off, so it never double-runs beside its original.
    • With no target, a packaged source lands in <base>.custom, through N-13's ensure door only.
    • A given target must be installed, writable and in reach. It shares the one target rule (new-similar-target.ts, landed by feat(runtime)!: the permission-set clone door lands "new similar" in a writable target package, by default the source package's .custom (#28563) #28636) with the permission-set clone door.
    • No package-less clone remains. An environment flow with no package is refused 400 VALIDATION_FAILED on package.
    • Rider 6554259755: Setup's "Open the clone in Studio" link reads the response package.
    • Clause-②: yes (narrowing), Metadata-protocol: added on both carriers. The PR body line was corrected by the seat after Check Changeset named the moved authorable-surface shard.
  3. Contract review: owed (a spec schema moves, and the PR narrows). PASS at CONTRACT_REVIEW_TIER (schema and narrowing faces) on this head, comment 6556965054. The route is mounted as well as ledgered.
  4. Gates: CI on head 387e88ca shows 58 success, 7 skipped and 0 red. The state is clean. Two spec-suite reds found by CI on 89a32773 (the contract count, the dropped-refinements header) are fixed at this head. The dev's full spec, clone and dogfood re-runs are green.
  5. Overlap: no conflict against main 6b9c1ca9. feat(runtime,spec): GET /packages/:id/customizations answers a base's overrides, extensions and switched-off items in one read #28595's and feat(runtime)!: the permission-set clone door lands "new similar" in a writable target package, by default the source package's .custom (#28563) #28636's hunks are byte-preserved.
  6. Landing: pr_ready + automerge_enable through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 11:15
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 11:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit f23e051 Nov 1, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-sdqbrt branch November 1, 2026 11:36
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 size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less

1 participant