Skip to content

feat(spec): ConnectorProviderContext.resolvePackagePath, a host-provided package anchor for provider factories (#22434) - #22465

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22434-provider-package-path
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22434-provider-package-path

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22434
Clause-②: yes

What this changes

ConnectorProviderContext (packages/spec/src/integration/connector-provider.ts) gains one optional, host-provided member, resolvePackagePath(relativePath), beside loadPackageFile. It takes the card's preferred, confined shape:

  • it resolves a relative path against the root of the stack/package that declared the connector entry and resolves to the absolute path;
  • '.' gives the root itself;
  • it refuses (rejects) an empty path, an absolute path (posix or Windows drive-letter) and a path that escapes the root after resolution (../x, a/../../x);
  • it reads nothing and does not check that the path exists.

Its signature is exactly its sibling's: (relativePath: string), resolving to a string. It is async for the same reason the loader is: service-automation imports node:path lazily, so constructing either capability never touches a Node builtin on a host without a filesystem. The consumer is an async provider factory, which awaits it.

@objectstack/service-automation's materializer hands it to every provider factory, right beside loadPackageFile (the providerCtx literal in plugin.ts), anchored at the same packageRoot option. The confinement check that lived inline in createPackageFileLoader is extracted into one private helper, resolveInsidePackageRoot, which both members call, so they cannot disagree about what is inside the root. loadPackageFile's behaviour and its three error strings (pinned verbatim by the docs/qa/platform-checklist/areas/integration-system.json item) are byte-identical; only the escape refusal's tail word is per member (reads are confined to for the loader, paths are confined to for the resolver).

createPackagePathResolver is exported from plugin.ts for its tests, and deliberately NOT from the package index: no caller outside the materializer needs to build one today, so the published surface of service-automation does not grow.

Not changed: packages/connectors/connector-mcp (using the member as the stdio transport's working directory is the next stage; #22423 remains open and carries those pins), the declarativeStdio policy (default deny, exact allowlist), and ADR-0097's text.

Measurements (the dispatch's four hypotheses, at da159f74e6)

  • H1, confirmed, with one sharpening. createPackageFileLoader (plugin.ts:206) held the whole rule inline: a non-empty check, an absolute check (path.isAbsolute plus a drive-letter regex), path.resolve(packageRoot ?? process.cwd()), then path.relative refused when it starts with .. or is absolute. Symlink escapes are not handled today: the check is lexical, with no realpath, so a symlink inside the root is judged by where it sits. The new member matches it exactly because it calls the same helper rather than a copy; a parity test pins "refused for confinement by both members or by neither" over 11 refs.
  • H2, confirmed. Every construction site of the context type in packages/ (tests included): one production host, the materializer in service-automation/src/plugin.ts, which now hands the member. Test-only sites: the ctx() helpers in connector-mcp/src/mcp-provider.test.ts, connector-openapi/src/openapi-provider.test.ts and connector-rest/src/rest-provider.test.ts. Each builds a context without the member, which still type-checks because it is optional, and none of those factories reads it, so none needs it. connector-materialization.test.ts and connector-reload-reingest.test.ts only receive contexts from the materializer.
  • H3, the note stays true and is not edited. packages/spec/liveness/connector.json's _note (and its README row) say loadPackageFile is host-injected, and that an authored entry reaches exactly the author-supplied context fields plus provider and enabled. A second host-injected member makes neither sentence false: it is not author-supplied and it reads no entry key. check:liveness is green on this head.
  • H4, consistent. The loader falls back to process.cwd() at call time when packageRoot is unset; the resolver does the same, through the same helper (pinned: an unanchored resolver resolves '.' to path.resolve(process.cwd())). '.' returns the root. For the loader, '.' passes the same confinement check and then fails to read a directory (could not be read), so the two members differ only where one reads and the other does not.

Pins

  • packages/services/service-automation/src/connector-materialization.test.ts, eight new cases:
    • the resolver: a relative path resolves under the root and '.' is the root (including an inside-landing ./specs/../specs and a path that does not exist yet); .., ../outside and specs/../../outside are refused, naming the ref and the root; an absolute path (posix, /etc, C:\evil) and an empty ref are refused; parity with the loader; the process.cwd() default;
    • the materializer: a factory resolving '.' and ./scripts/fixture.mjs gets locations under packageRoot; a factory resolving ../outside fails boot loudly with the confinement refusal; a factory that never reads the member materializes exactly as before.
    • Each refusal case also asserts the rejection is not a TypeError, so a missing member cannot pass for a refusal.
  • packages/spec/src/integration/connector-provider.test.ts: type-level pins that the member is optional ({} extends Pick alias, which toEqualTypeOf cannot tell apart from a required member typed with undefined) and has exactly loadPackageFile's type, plus a context literal without the member that must keep compiling. check:test-typecheck compiles this file.

One-off proofs (run against committed head 159042aed0, mutated through scripts/ablation-replace.mjs, every leg restored and proven: blob equals HEAD, git diff HEAD empty)

leg mutation expected observed
RV1 (reverse verification) the materializer hands this.options.packageRoot (a string) as the member service-automation tsc reads the rebuilt spec .d.ts and refuses it red: src/plugin.ts(2022,17): error TS2322: Type 'string or undefined' is not assignable to type '((relativePath: string) =) Promise(string)) or undefined' (angle brackets and pipes respelled here)
A1 the spec member made required the optionality pins go red red: check:test-typecheck names src/integration/connector-provider.test.ts with 4 type errors
A2 the resolver bypasses the helper (bare path.resolve) the escape, absolute/empty, parity and escape-at-boot cases go red red: 4 failed, 46 passed; exactly those four cases
A3 the materializer anchors the resolver at process.cwd() only the anchored-locations case goes red red: 1 failed, 49 passed; the one red is that case

Verification

All of the following ran on head 159042aed0 (branch point da159f74e6).

  • Build. pnpm turbo run build --filter='@objectstack/service-automation...' --concurrency=2, under the verify lock: 30 of 30 tasks successful, @objectstack/spec and @objectstack/service-automation rebuilt (cache misses). The rebuilt packages/spec/dist carries resolvePackagePath.
  • Tests. @objectstack/service-automation (vitest run --maxWorkers=2): 178 files, 2187 tests passed. The touched file run verbose: 50 passed, the eight new cases among them. @objectstack/spec (vitest run --project local --maxWorkers=2): 627 files, 18758 passed, 1 todo. connector-provider.test.ts run verbose: 15 passed.
  • Typecheck. pnpm --filter @objectstack/service-automation typecheck and pnpm --filter @objectstack/spec typecheck (tsc, the scripts program, check:test-typecheck): exit 0, both.
  • Gates. node scripts/pm/dispatch-gates.mjs --commands, re-derived on the actual diff: 87 families. That is the dispatch's list plus six families the test-file edits add: check:engine-double-contract, check:objectql-double-limit, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher. 86 exited 0. check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET: it reads every package's built output, and only this diff's 30-package closure was built. It is NOT MEASURED here and left to CI. dispatch-gates --ran answers: "87 derived famil(ies) accounted for — 86 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
  • Changeset level axis, driven offline with a payload carrying this body's first two lines: "this PR declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch".
  • Lint, narrowed (a measurement, not a skip). eslint --no-inline-config --format json over the four changed TypeScript files: 4 files, 0 errors, 0 warnings. All four are in the linted population (--print-config returns a rule set for each). eslint.config.mjs enables no type-aware linting (no parserOptions.project), so this diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's.
  • Branch freshness. origin/main has moved four commits past the branch point (to 3ca71b6e05). None of them touches the five paths in this diff, and a local merge-tree merges clean. CI runs on the merge ref.

Acceptance notes

  • Lexical confinement (both members). Neither member follows symlinks when judging confinement, which is the family's rule as it stood. The authoring doors (defineStack, PUT /meta/connector/:name) can set a path but cannot create a symlink, so the declared threat (an entry reaching outside its package) is covered; a realpath rule would change loadPackageFile's behaviour and is not this card. Noted, not filed. Carrier: none.
  • ..-prefixed names are refused although they stay inside the root. path.relative of a file named ..foo under the root is ..foo, which the startsWith('..') test refuses. It is loud (a refusal, never a silent read), the loader has always done it, and it is kept byte-identical here so the two members stay one rule. Noted, not filed. Carrier: none.
  • ADR-0097 describes loadPackageFile only (docs/adr/0097-declarative-connector-instances.md, the providerConfig.spec bullet). A sentence for the sibling would be a governed (Tier H) edit, so it is left out of this PR. Carrier: none.
  • Platform checklist. The integration-system item pins the loader's three strings, which are unchanged. No checklist item covers the resolver yet; the user-visible behaviour arrives with the connector-mcp stage. Carrier: connector-mcp: a declarative stdio transport resolves its relative command against the process cwd, while the same app's OpenAPI file ref resolves against the package root — showcase_mcp_tools boots degraded from any other directory #22423.

Generated by Claude Code

…ded package anchor

ConnectorProviderContext gains one optional, host-provided member,
resolvePackagePath(relativePath), beside loadPackageFile. It resolves a
relative path against the declaring stack/package root, returns the root
for '.', and refuses an empty, absolute or root-escaping path.

service-automation's materializer hands it to every provider factory,
anchored at packageRoot (default process.cwd()), and both members now
share one confinement check (resolveInsidePackageRoot) so they cannot
disagree. loadPackageFile's behaviour and error strings are unchanged.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 9, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec, touching 7 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/connectors.mdx (via AutomationServicePluginOptions (symbol, a top-level interface))
What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 139 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 3ca71b6e05efbfc6ec5908c8c263fee6cceba389 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3ca71b6e05efbfc6ec5908c8c263fee6cceba389

⚠️ 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 3ca71b6e05efbfc6ec5908c8c263fee6cceba389 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 159042aed0b51fa7d901a44e0da62ad694441f8e
Local-runs: none

Inputs, and nothing else: card #22434 (body, the Claim comment, the os-dev-report comment), PR #22465 (body, its five-file list, the net diff against main from the merge-base), and the check-runs on the head. Check-runs read 2026-10-09T11:05Z: 32 runs, 17 completed success, 3 completed skipped (Console Pin Gate, Build Docs, Packed-tarball smoke), 12 in progress. Still running at that read, so NOT judged green here: Lint and Repo Gates, Type Check workspace, Type Check consumer gates (which hosts check:api-surface), Test Core (all six shards), Dogfood Regression Gate (1/3 and 3/3), Temporal Conformance. Their conclusions are the gate verdicts; this record pre-judges none of them. Completed success at that read, named because sections ① and ② lean on them: Type Check source gates (check:authorable-surface, check:docs, the check:generated reconcile), Type Check debt ledger, Spec property liveness, Check Changeset (the level axis), Build Core (which hosts check:dual-build-cjs-loads), Governed Surface Queue Guard, Check PR Size, and the claim and closing-keyword guards.

① Derived judgments

  1. Public surface, @objectstack/spec: ConnectorProviderContext gains ONE optional readonly member, resolvePackagePath, typed exactly as loadPackageFile (one string argument, resolving to a string; the spec test pins the equality with a type alias and the optionality with an {} extends Pick alias). Additive and optional: every context that conformed before still conforms, which the three connector packages' test ctx() helpers (left untouched) demonstrate. RIGHT. No generated artifact moves: api-surface/integration.json records the interface by name only, ADR anchors name neither touched file, and no hand-written doc or published skill enumerates the context's members (the only mentions outside src are the liveness note and README row, ADR-0097 and the platform checklist, all intact and all still true as written).
  2. Accept set of the new member, as the spec docblock declares it and the host enforces it: a relative path resolves under the root; '.' is the root; an empty or whitespace path, an absolute path (posix or drive-letter) and a path that escapes the root after normalization are refused; no read, no existence check. Each is pinned in connector-materialization.test.ts, each refusal with a not-a-TypeError assertion so an absent member cannot pass for a refusal. RIGHT: this is the card's preferred confined shape; the raw packageRoot alternative was not taken, so no confinement exception is owed.
  3. Accept set of loadPackageFile: unchanged. The inline check moved into resolveInsidePackageRoot, called with 'reads'; the three error strings compared against the removed lines and against the platform checklist's two pinned clauses are byte-identical, and the check order is the same. RIGHT. One rule behind two members is the correct construction, and the parity case over eleven refs pins it.
  4. Host, @objectstack/service-automation: the materializer hands resolvePackagePath beside loadPackageFile at the one production construction site (the providerCtx literal), anchored to the same packageRoot with the same process.cwd() fallback; pinned by the anchored-locations and escape-at-boot cases. RIGHT. createPackagePathResolver is exported from plugin.ts for its test and NOT from the package index, so the package's published surface grows by behaviour (every factory now receives the member), not by an export. RIGHT, with one observation: createPackageFileLoader IS on the index (no importer outside the package exists), so a host that builds its own context has the loader builder and not the resolver builder. Not a defect; a host that needs it adds the export under its own changeset.
  5. Wording: the resolver's refusals reuse the loader's package file ref prefix for an input that is a path, not a file ref. Cosmetic, outside the contract, pinned by no test beyond two substrings.
  6. File surface held: exactly the five paths the Claim named. connector-mcp and the declarativeStdio policy are untouched; connector-mcp: a declarative stdio transport resolves its relative command against the process cwd, while the same app's OpenAPI file ref resolves against the package root — showcase_mcp_tools boots degraded from any other directory #22423 keeps its stage.

② Semver level

  • Changeset .changeset/22434-connector-provider-package-path.md: @objectstack/spec minor, @objectstack/service-automation minor. RIGHT. A new optional member on a published interface is a purely additive widening, at least minor; the host now hands every factory a member it did not before, a feature in a released package, minor. Nothing is removed or renamed, so no FROM to TO migration text and no ADR-0087 disposition marker is owed, and the changeset's "Nothing to migrate" paragraph is accurate.
  • Clause-②: line: the PR body carries Clause-②: yes, bare, on its own line; the changeset carries Clause-②: yes (widening); the card declared the same. Per the fleet's one reader (scripts/pm/clause2-line.mjs) these are one declaration in two spellings, both at least minor; Check Changeset concluded success on this head. RIGHT.
  • Not skip-changeset: the diff publishes from two released packages. RIGHT.

③ Boundary flags

The os-dev-report carries no open_questions. Its five deviations and four out-of-scope findings:

  • D1, no pull of main and no full suite before the PR. Verified: the commits from the merge-base to origin/main are the four the report names and none touches the five paths; PR checks and the queue run the merge ref. Answered: accepted.
  • D2, check:dual-build-cjs-loads NOT MEASURED locally. Hosted by ci.yml's Build Core, completed success at my read. Answered: discharged by the check-run.
  • D3, commit trailers in AGENTS.md's model-free pair rather than the harness's model-named form. Verified on the head commit (Claude-Session plus Co-authored-by: Claude). AGENTS.md and the pre-push hook govern. Answered: correct, not a deviation.
  • D4, the spec-side pin is type-level only. Prime Directive 2 keeps logic out of spec, so the behaviour pins belong with the host that implements the member. The card's third pin (absence leaves today's behaviour unchanged) is covered by the type-level optionality pin, the never-reads-it materialization case, and the three untouched connector tests that build contexts without the member. Answered: accepted.
  • D5, the worktree was created off the named branch point. Procedural. Answered: no contract effect.
  • F1, lexical confinement with no realpath, both members. The spec docblock now says the check is on the normalized path, so the declared claim is exactly as narrow as the enforcement, and the authoring doors cannot create a symlink. Answered: a note, not a card; a realpath rule would change the loader and is its own decision.
  • F2, a file named ..foo inside the root is refused. ESCALATED to the dispatching seat as a filing candidate: a reproducible refusal of an input the docblock says is accepted (one-line repro; the fix tests for .. alone or .. followed by a separator), pre-existing since feat(connector-openapi): declarative provider — resolve providerConfig.spec from a file path (ADR-0096 follow-up) #3016 and deliberately kept byte-identical here so the two members stay one rule. Not blocking this PR.
  • F3, ADR-0097's text names only loadPackageFile. A sentence for the sibling is a Tier H edit that would govern the whole PR; an optional sibling reverses no ADR-0097 decision, so no superseding ADR is owed. Answered: correctly kept out.
  • F4, no platform-checklist item for the resolver; carrier connector-mcp: a declarative stdio transport resolves its relative command against the process cwd, while the same app's OpenAPI file ref resolves against the package root — showcase_mcp_tools boots degraded from any other directory #22423. The user-visible behaviour arrives with the connector-mcp stage. Answered: accepted.

Implemented-by: claude/issue-22434-provider-package-path
Reviewed-by: session_01VZqqwTj2wsihZEbfT6yyYN

VERDICT: PASS


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 37931072987 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/verify-host-root.test.ts > os verify anchors the app at its own directory, not the process cwd (#22301) > loads the app's own plugin from the app's dir
      ↳ 失败原因: @objectstack/cli:test: AssertionError: os verify failed from a foreign cwd:
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 12 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants