Skip to content

fix(cli): os package publish honours or refuses a declared manifest.id, never substitutes one - #17530

Merged
os-justin merged 3 commits into
mainfrom
claude/issue-16891-publish-manifest-id-loud
Sep 10, 2026
Merged

fix(cli): os package publish honours or refuses a declared manifest.id, never substitutes one#17530
os-justin merged 3 commits into
mainfrom
claude/issue-16891-publish-manifest-id-loud

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #16891

os package publish no longer publishes under a manifest id the author did not write. A manifest.id the compiled artifact declares is now used or refused — never silently swapped for a derived one.

What was wrong

deriveManifestId adopted artifact.manifest.id only when it parsed as PackageSchema.manifestId. Any other declared value fell straight through to local.SLUG-OF-MANIFEST-NAME and published under that, saying nothing: the substituted id appeared in the ordinary progress line, byte-identical to the run where the artifact declared no id at all.

sys_package.manifest_id is documented immutable once set — "renaming a package requires creating a new package" — so the value chosen there is a permanent, globally unique identifier. The author's own declaration was the one input the command was sure not to use.

The change

deriveManifestId treats a present, non-blank string manifest.id as a declaration, and hands it to the single preflight gate in run(). If it is not an id the control plane accepts, the publish refuses there, before any network call, quoting the schema's own issue and description. manifestIdRemedy gains a distinct line for that source, naming the conflict: the value is the author's, the command will not substitute for it, and the three ways out are fixing manifest.id, removing it, or passing --manifest-id.

No second manifest-id rule is introduced in the CLI. The judgement is still PackageSchema.manifestId, imported not transcribed — the source pin in the suite (no anchored regex in the command source that matches a legal id) still holds.

An absent, blank or non-string manifest.id is not a declaration and derives exactly as before.

Acceptance notes

The card's reproduction, driven before and after

Driven through the real command with fetch stubbed, on this branch's tree — case A is the card's repro, case B the control it was indistinguishable from:

before (e7e883c1) after
A — artifact declares manifest.id = 'crm', manifest.name = 'Acme CRM' exit 0 · wire manifest_id: local.acme-crm · prints Registering package 'local.acme-crm'... exit 1 · no network call · prints Invalid manifest-id 'crm'. …
B — artifact declares no id, manifest.name = 'Acme CRM' exit 0 · wire manifest_id: local.acme-crm · prints Registering package 'local.acme-crm'... unchanged

Before, A and B produced byte-identical output. That indistinguishability is the defect, and it is now pinned as one assertion over both runs (a discarded declaration is distinguishable from no declaration) rather than as two separate expectations that could drift apart.

The full refusal A now prints:

✗ Invalid manifest-id 'crm'. Invalid string: must match pattern
  /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/. Expected: Globally unique
  reverse-domain package identifier (e.g. com.acme.crm). It is the
  `manifest.id` declared by the compiled artifact, and `os package publish`
  publishes under the id you declared or refuses — it does not substitute a
  derived one, because manifest_id is immutable once published. Fix
  `manifest.id` in objectstack.config.ts and rebuild, remove it to derive an
  id from `manifest.name`, or pass --manifest-id.

Why the other branch was not available

Triage allowed two outcomes — honour the declared id, or refuse naming the conflict — and preferred honouring. Honouring is not available here, and not for the reason triage's warning anticipated:

  • It is not that something downstream depends on the derivation. deriveManifestId has exactly one non-test caller, run() in this same file (repo-wide git grep); nothing else reads the local.… shape or the ManifestIdSource tag.
  • It is that the values that reached the fall-through are, by construction, exactly the ones the control plane refuses. The old step-1 gate was PackageSchema.manifestId.safeParse(...).success, so a value fell through if and only if that schema rejected it. And CreatePackageRequestSchema.manifestId is not a copy of that rule — it is that schema node (manifestId: PackageSchema.shape.manifestId, packages/spec/src/marketplace/package.zod.ts), which is the declared shape of the manifest_id this command POSTs to /cloud/packages. Forwarding one would not honour it; it would relocate the identical refusal to the server, later, with a worse message, after the artifact preflight.

So honouring is already what happens for every id the control plane accepts — that path is untouched, bytes unchanged — and refusal is the only remaining outcome for the rest. Keeping the substitution and logging a warning was foreclosed by the ruling and is not implemented.

Where the judgement comes from

The card's sharpest observation is that ManifestSchema.id is a bare z.string() whose "reverse domain style" shape lives only in TSDoc prose, so there is no machine-readable declaration of a usable id on the manifest side. This change invents no second charset in the CLI to fill that gap. The authority it uses is PackageSchema.manifestId — the schema for the very column being published into, and the same node the control plane's own CreatePackageRequestSchema declares. That is the only rule the command asks, on both the explicit and the derived path, and the suite's source pin keeps it that way.

Making ManifestSchema.id itself enforce its prose (the card's option 3) is a spec narrowing on an authorable key with values in the wild, and is left to the spec seat — packages/spec is untouched by this diff.

Coordinates re-derived (card measured at c930f8597; today bea76c928)

Coordinate At the card Today Moved
packages/cli/src/commands/package/publish.ts present present no
step-1 gate in deriveManifestId MANIFEST_ID_RE.test(explicit) && explicit.includes('.') isManifestId(explicit) yes#16889 landed the narrowing the card called #16520
MANIFEST_ID_RE local transcription present deleted yes
isManifestId / explainManifestId / manifestIdRemedy absent present new since the card
PackageSchema.manifestId packages/spec/src/cloud/package.zod.ts packages/spec/src/marketplace/package.zod.ts yes#17372 dropped the /cloud subpath
ManifestSchema.id (kernel/manifest.zod.ts) bare z.string(), line 252 bare z.string(), line 260 line only
the card's reproduction prints local.acme-crm still printed local.acme-crm before this diff no — defect live

The card's own prediction is confirmed by measurement: because the step-1 gate is now the schema, the set of values taking the silent-substitution path got larger, not smaller. All six shapes the retired local rule admitted (com.acme.repair_desk, COM.ACME.CRM, 9foo.bar, com..acme, com.acme., and crm) were being silently substituted on the derive path as of this branch's base.

Reverse verification

Predicted direction: red. Reverting only the declaration branch in deriveManifestId back to isManifestId(explicit) (mutation proven on disk by anchor counts and a changed blob hash; restore proven by blob hash equality to HEAD) turns 7 of 27 tests in the suite red — the six declared-shape refusals and the distinguishability pin — and green again on restore. The suite reads ../src/commands/package/publish.js relatively, so no build stands between the mutation and the assertion.

Noted, not filed

  • The file header's TSDoc still cites CreatePackageRequestSchema in cloud/package.zod.ts; feat(spec)!: drop the @objectstack/spec/cloud subpath — control-plane contracts leave, the package format moves to @objectstack/spec/marketplace #17372 moved it to marketplace/package.zod.ts. Prose path drift, not a copyable example. Carrier: the next PR touching manifestIdSchema in this file.
  • A non-string manifest.id (say 42) is still dropped without a word. It is off-spec against ManifestSchema.id rather than a legal key being discarded, and publish.ts never parses the artifact through ManifestSchema at all. Carrier: a card that makes the publish path parse the artifact manifest through its own schema.
  • manifestId in objectstack.manifest.json outranks a declared manifest.id with no word when the two disagree. That is documented precedence between two author-written declarations, not a substitution the CLI invents. Carrier: the same publish path.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands over the three changed paths derived 63 families; all 63 run, reconciled with --ran carrying every exit code — 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, no STALE TREE.
  • pnpm lint (whole tree, eslint . --no-inline-config) — exit 0, no narrowing applied.
  • pnpm --filter @objectstack/cli exec vitest run --project unit — 194 files / 2689 tests pass. The integration tier is declared to CI: the diff touches no spawn entry, no bin/, no test/helpers/serve-process.ts and no driver/kernel boot path.
  • pnpm --filter @objectstack/cli typecheck — exit 0; the edited test file is in the type-checked program (confirmed by tsc -p tsconfig.test.json --listFiles).
  • pnpm --filter @objectstack/spec check:generated after merging origin/main — all 15 generated artifacts up to date.
  • Deviation, declared: pnpm check:type-check-debt OOMs at the standard --max-old-space-size=4096 (its whole-ledger tsc re-measure), and was re-run at 7168 where it exits 05 ledger entr(ies) re-measured, 55 raw tsc error(s), none above its recorded number. Its population is the 4 debt-ledger packages; packages/cli is type-checked and not among them, so this diff cannot move it.

Clause-②: yes — this narrows the accept set of a published command. An os package publish invocation that exits 0 today (an artifact carrying manifest.id = 'crm') exits 1 after this change. The subtlety worth the reviewer's attention: the narrowing goes past what is actually declared. ManifestSchema.id is a bare z.string(), so 'crm' is a perfectly legal manifest.id against its own schema; what refuses it is PackageSchema.manifestId, the schema of the column it would be published into. Nothing is removed or renamed and no key becomes unwritable — an author may still write any manifest.id — but a publish that previously succeeded under a substituted id now fails, so this is declared yes rather than argued down.

Changeset: patch on @objectstack/cli, carrying the before/after and the three ways out. Same level as #16889, the sibling narrowing of this same command's accept set.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt


Generated by Claude Code

…t.id`, never substitutes one

A `manifest.id` the artifact declares that is not a manifest id the control
plane accepts used to fall through to `local.<manifest.name slug>` and publish
under it silently. `sys_package.manifest_id` is immutable once set, so the
author acquired a permanent, globally unique identifier they never wrote and
cannot rename — and the substituted id appeared in the ordinary progress line,
byte-identical to the run where the artifact declared no id at all.

A declared id is now carried to the existing preflight gate and refused there
by name, quoting the schema. Honouring it instead is not available: the values
that reached the fall-through are exactly the ones `PackageSchema.manifestId`
rejects, and `CreatePackageRequestSchema.manifestId` is that same schema node,
so forwarding one would only relocate the refusal to the server.

Absent, blank and non-string `manifest.id` are not declarations and derive as
before. No second manifest-id rule is introduced in the CLI.

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

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 3 documentable anchor(s).

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

  • content/docs/api/declarative-endpoints.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/concepts/metadata-lifecycle.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/cli.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/deployment/publish-and-preview.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/kernel/metadata-service.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/objectql/index.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))
  • content/docs/protocol/objectql/schema.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v9.mdx (via os package publish (command, read off packages/cli/src/commands/package/publish.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 23 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 efa2533dd88a6c8304761253c9916f81699c3dc9packageMentionDocs.

Which tree this was computed on

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

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

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

Copy link
Copy Markdown
Collaborator Author

Seat note — one convergence red, patch round with the same dev; clause-② review still pending

Not a review verdict. The in-seat clause-② review this PR needs (declaration leg hit ⇒ 「无席内条款②复核 PASS 在案 ⛔ 禁止入队」) is not written yet, and ⛔ this PR is barred from the queue until it is. This note records state so the carrier is not read as idle.

The red

Check Changeset is failure at head 2cf5fdae (run 34523127214). Read from the job log, ⛔ not inferred: the PR declares Clause-②: yes and carries needs:contract-review, so the level axis requires at least one package at minor; the changeset grades @objectstack/cli at patch.

The gate names two non-interchangeable ways out — raise the level, or correct the declaration at the producer — and warns ⛔ 「Do not add a tolerance here to route around a declaration that says something its author did not mean」.

Which one, and why — because way 2 was genuinely arguable

I considered correcting the declaration to no. The rule defines clause-② as widening (「放宽接受集或扩大公开面」) and says narrowing does not trip it (「收窄仍是语义面,不触条款②」). And this PR's own central measurement makes a real case that it is a pull-back rather than a narrowing: the values it now refuses are by construction exactly those the control plane already refuses.

⭐ I verified that at source rather than taking the PR's word — packages/spec/src/marketplace/package.zod.ts:284 reads manifestId: PackageSchema.shape.manifestId, so CreatePackageRequestSchema.manifestId is that node and not a copy. The honour-branch foreclosure argument holds, and it is a stronger argument than the one triage anticipated.

Keeping yes anyway, on two rule-level grounds rather than code-level ones:

  1. The protocol names a wrong no as the auditable failure (「错误的 no 是可审计的假申报」) and names no matching cost for a conservative yes; claim-time guidance is 「拿不准 ⇒ 按 yes」. This sits exactly on the boundary that guidance exists for — a narrowing that goes past what is declared, which the PR body flags itself: ManifestSchema.id is a bare z.string(), so 'crm' is a legal manifest.id; what refuses it is the schema of the column it would be published into.
  2. Independently of clause-②, minor is the honest level. An os package publish that exits 0 today exits 1 after this change — patch understates that to the changelog's reader, who is the audience the level serves.

⇒ The level was owed anyway, so way 1 costs nothing and way 2 would have traded a correct level for a contestable declaration. Patch round with the same dev (「收敛期转红走补丁轮续派原 dev,⛔ 不作 REWORK 的理由」): raise @objectstack/cli to minor, change nothing else.

What already reviews clean at this head

Closing keyword safe on both reads — only Fixes #16891; #16889, #16520 and #17372 each sit with no verb beside them. 0 governed-surface paths. packages/spec untouched ⇒ the clause-② path leg is unhit and only the declaration leg is live. Carriers correctly on both card and PR. Commit status success, not pending.

⚠️ 16 check families were still in_progress when I read this head, so this is ⛔ not a complete CI verdict — gate in_progress at draft time is an honest reading, not a green one.

One cosmetic, recorded so it is not mistaken for the platform's doing

This body carries two attribution blocks — the 🤖 Generated with … two-liner stacked above the --- + session-URL footer. That is not a platform append: references/platform-readings.md records that the append fires only when the sent body's tail is not already a rule-plus-footer block, and that the stacked shape stores as multiple blocks (「单块形态才复现成一条」). ⚠️ It is also ⛔ not safely fixable by reflex — the same file records that one channel deletes a PR body's footer block and another appends a bare one, and that the deleting cell is 「按最坏走」. Left to the dev to fix only if the recorded cell for its channel makes it safe, and to leave and say so if not.

派发席位 · session_01DapQyvYrFb1MxSYe7BL2nt · R72 · 2026-09-10T20:02Z · 本评论来自 domain:cli 派发座位


Generated by Claude Code

…his PR's clause-② declaration

`Check Changeset`'s level axis refuses a PR that declares clause-② `yes` while
grading no package whose `packages/**/src/**` it moves at `minor` or above. This
PR declares `yes` on both carriers and moves only `@objectstack/cli`, so that
package carries the level.

`minor` is also the honest level on its own merits: an `os package publish`
invocation that exits 0 today exits 1 after this change, and `patch` understates
that to the reader of the changelog, which is the audience the level exists for.

The declaration, the carrier label and the PR body's `Clause-②: yes` line are
deliberately unchanged — the level was wrong, not the declaration.

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

Copy link
Copy Markdown
Collaborator Author

Contract review

Reviewed head: 636ec29402 (636ec29402..., PR #17530 / card #16891).

⚠️ The patch round moved the head from 2cf5fdae, so I re-derived rather than carrying the old reading forward. git diff 2cf5fdae 636ec294 is 1 file changed, 1 insertion(+), 1 deletion(-) — the changeset frontmatter alone. The src and test diff is byte-identical to the tree I read line by line at 2cf5fdae, which is why the judgments below transfer without re-reading them.

Tier: default judgment tier, self-review plus gates — 「余席条款②复核 = 默认判断档自审加门禁」. Contract-review tier is ⛔ reserved to the skills and spec seats and is not reachable from this seat.

① Derived judgments — each accept-set and public-surface change the diff produces, named and judged

  1. deriveManifestId step 1 now treats a present, non-blank string artifact.manifest.id as a declaration and routes it to the preflight gate instead of falling through to local.SLUG-OF-MANIFEST-NAME. ⇒ an invocation that exits 0 today exits 1. Accept-set change: a narrowing of the invocation set. CORRECT — it is the card's ask, and triage's two allowed outcomes reduce to this one (see 3).
  2. Public surface: unchanged. Measured, ⛔ not reasoned: the export set at origin/main and at 636ec294 is the same six — isManifestId, NAMESPACE_RE, ManifestIdSource, DerivedManifestId, deriveManifestId, default class PackagePublish. Only line numbers moved (deriveManifestId :132:150). No symbol added. CORRECT.
  3. No new key on a published payload. The POST body still carries the single manifest_id: manifestId (:515) into POST /cloud/packages. What changed is which value may reach it, never the shape. CORRECT.
  4. ManifestIdSource gains no member — the union is still 'artifact-manifest-id' | 'artifact-manifest-name' | 'artifact-filename'. manifestIdRemedy splits its message by a member that already existed. Message text, not surface. CORRECT.
  5. The refusal authority is imported, not transcribed. ⭐ Verified at source rather than from the report: packages/spec/src/marketplace/package.zod.ts:284 reads manifestId: PackageSchema.shape.manifestId, so CreatePackageRequestSchema.manifestId is that node and not a copy of it. ⇒ the CLI invents no second manifest-id charset, and the suite's source pin (no anchored regex in the command matching a legal id) still holds. CORRECT, and it is the load-bearing fact of this PR.
  6. ⚠️ The one that must be named rather than waved through: the narrowing goes PAST what is declared. ManifestSchema.id is a bare z.string(), so 'crm' is a perfectly legal manifest.id against its own schema; what refuses it is the schema of the column it would be published into. CORRECTsys_package.manifest_id is documented immutable once set, so a substituted value is a permanent wrong identifier, and the values now refused are by construction exactly those the control plane already refuses (see 5), meaning nothing that succeeded end-to-end stops succeeding. But this is precisely why the declaration is yes and ⛔ not argued down, and it is the sentence a future reader should be handed first.

Machine predicates, both read at this head: check-clause2-carriers --pair 17530 ⇒ ✓ 「the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell」 — quoting its own caveat rather than hiding it: 「A tell is not a proof and its absence is not one either」. check-widening-tells --declaration yes --diff ⇒ ✓ 「a yes already routes to contract review, so a tell on top of it decides nothing」 (261-line diff supplied; the gate refuses to answer without both a diff and a declaration, and ⛔ silence is not a clearance).

② Semver grading — level consistent with the declaration

@objectstack/cli: minor, verified in the file at this head. Consistent. The level axis requires at least one package at minor on a clause-② yes PR, and @objectstack/cli is the only package this diff moves.

⭐ It is also right on its own merits, independently of the gate: an os package publish that exits 0 today exits 1 after this, so patch would understate it to the changelog's reader. ⛔ Not major: nothing is removed or renamed and no key becomes unwritable — an author may still write any manifest.id.

The original patch was a reasonable reading (the dev cited #16889, the sibling narrowing of this same command) and the correction cost one CI cycle, ⛔ not a delivery defect.

③ Boundary flags — every declared deviation and out-of-scope note, answered

Deviations (5):

  1. Heap raised for check:type-check-debt (OOM at 4096, exits 0 at 7168). Accepted. Its population is the four debt-ledger packages and packages/cli is not among them, so this diff cannot move it; the --ran record's 0 is the 7168 run and is labelled as such. ⛔ No gate was weakened — the memory ceiling is not the gate.
  2. Full workspace build run locally, twice. Acceptedcheck:i18n-coverage and check:dual-build-cjs-loads answer PREREQUISITE NOT MET until every package has a dist, and 「PREREQUISITE NOT MET」 is ⛔ not a pass. Re-building after the origin/main merge is the documented sequence.
  3. The dev hung needs:contract-review itself, although my dispatch said I would.The dev was right and my dispatch was wrong. references/contract-review.md: 「needs:contract-review(恒英文)由 PR 创建者随可复审契约增量同笔挂:draft PR,或先到的报告」 — the PR creator hangs it. Its stated reasoning (「an over-hung gate is safe while an un-hung one is not」) matches 「PR 一存在即挂」. Accepted, and the misassignment was mine.
  4. Clause-② re-declared yes from the delivered diff against the claim's provisional no. Accepted — this is the mechanism working as designed. My claim pre-registered both branches and said the refuse branch 「would narrow past what is actually declared … which is exactly why the declaration has to come from the delivered diff」. It did.
  5. Platform footer append on PR create and on the report comment. Accepted, with one precision. The create-side append is the documented fourth form (references/platform-readings.md): both channels append when the sent body's tail is not already a rule-plus-footer block. ⚠️ This body additionally stacks the harness two-line block above the footer, which that file records as storing multiple attribution blocks (「单块形态才复现成一条」). ⛔ Left unfixed deliberately: the same file records that one channel deletes a PR body's footer block and that the deleting cell is 「按最坏走」, so an edit risks a worse shape than a cosmetic duplicate. ⛔ Not a defect in the delivery.

Out-of-scope notes (4), each with its disposition:

Independence

Implemented-by: claude/issue-16891-publish-manifest-id-loud
Reviewed-by:    session_01DapQyvYrFb1MxSYe7BL2nt

⚠️ Stated plainly so a reader can weigh it: the implementer was a mode:subagent dev dispatched by this seat, which is why the rule spells its identity as a branch (「子代理无自有 session」) rather than a session id. This is a default-tier in-seat review with the gates, ⛔ not an isolated adversarial one — that instrument (「独立性件」) is the spec seat's, for a genuine contract fork or an unresolved dev flag. Neither is present: every flag above is answered, and the one judgment I overrode (the changeset level) I overrode toward the stricter reading, ⛔ not the looser.

Verdict

PASS.

This clears ① of the three pre-landing checks only. ② is the carrier strip plus a --pair re-read, done in the next stroke; ③ is every check green, and 14 families were still in_progress at this head when I read it ⇒ ⛔ not enqueued yet, and 「gate in_progress 是诚实读数」, not a green one.

派发席位 · session_01DapQyvYrFb1MxSYe7BL2nt · R72 · 2026-09-10T20:24Z · 本评论来自 domain:cli 派发座位


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Carrier strip was mine — and two corrections the dev made to me, both right

The needs:contract-review strip at 20:10:26Z was deliberate, by this seat

The dev measured the unlabel, correctly ruled out a size-labeler collision (github-actions[bot] only ever emitted labeled, at 19:53Z), correctly declined to re-hang it, and reported it instead. ⭐ That is exactly the right handling — 「载体是复核席的,清与挂都归它」 — and I am confirming rather than leaving it ambiguous.

It follows the contract-review PASS recorded above (comment 5624785327, head 636ec294): 「清标即落地:PASS ⇒ 同席同笔剥双载体」. Provenance is on the card as comment 5624795523; recording it here too so the PR side is not left guessing. The timeline reads labeled … os-justin 19:54:04Z (the dev, per 「PR 一存在即挂」) → unlabeled … os-justin 20:10:26Z (this seat, on PASS), which is the intended shape and ⛔ not a half-state.

⭐ The dev also measured the consequence rather than assuming it: the level axis still exits 0 against the carrier-less label set, because the body's Clause-②: yes line is read as the declaration on its own. ⇒ the strip did not paper over the gate.

My footer reasoning was inverted — the dev's original deviation note was accurate

In my earlier seat note I wrote that the platform 「appends only when the sent body's tail is not already a rule-plus-footer block … and yours was」, and concluded the harness two-liner could not have been an append.

That inference is backwards. The dev's sent tail was the harness two-liner, which is not a rule-plus-footer block — so platform-readings.md:323's fourth form fired exactly as written and appended one block. ⇒ its original 「PLATFORM FOOTER APPEND」 note was correct and my 「it is not」 was wrong. The half I got right is only that the two-liner came from the sent body; the half I got wrong is the one that mattered.

The stored shape is three attribution blocks, which is :329's recorded outcome verbatim (「正文把 harness 两行块叠在页脚之上,存回是三条署名块」). ⚠️ My own earlier count of "two" came from a regex matching Generated (by|with), which collapses the harness two-liner into one — a counting-convention difference, ⛔ not a second reading of the same thing.

And the dev found the reason it must be left alone, which I had not: MCP update_pull_request deletes the footer block (:319, the cell marked 「按最坏走」 at :328) and always sends the draft bit — and AGENTS.md is explicit that a flipped-back draft 「destroys auto-merge and queue membership at once … invisibly」. Raw REST PATCH /pulls appends a bare footer on top (:320), taking three blocks to four. ⇒ neither channel lands the single-block form, and AGENTS.md separately forbids re-sending a body that already carries an appended footer. Left, cosmetic, reported. That draft-bit consequence is the sharpest thing in this exchange and it is the dev's, not mine.

One more of mine, since the record should be checkable

⚠️ The 20:24Z / 20:27Z timestamps in my last few seat-comment footers are wrong — the API created-at times are 20:10:15Z and 20:11:08Z. I was stamping estimates instead of reading the clock, and they ran roughly fifteen minutes fast. The API timestamp beside each comment is the authority; the stamps in my footers are ⛔ not, for anything posted before this one. Reading the clock from here.

Landing state

Pre-landing, all three now satisfied and each re-measured by me at head 636ec2940279fea170c2dd20d798612f7a4c08bc, ⛔ not taken from the report: ① the contract-review PASS above; ② carriers cleared, check-clause2-carriers --pair 17530 exit 0; ③ 33 check runs collapsed latest-per-name — 28 success, 5 skipped, 0 not-green, every one carrying this head_sha, Lint & Repo Gates / TypeScript Type Check / Check Changeset all success, commit status success, mergeable_state: clean, 0 governed-surface paths.

Flipping ready and arming auto-merge. The queue is the only landing path and ⛔ this seat never merges outside it; pm:dispatched stays on #16891 until MERGED is verified by measurement on origin/main.

派发席位 · session_01DapQyvYrFb1MxSYe7BL2nt · R72 · 2026-09-10T20:32Z(读表,非估计) · 本评论来自 domain:cli 派发座位


Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 10, 2026 20:32
@os-justin
os-justin enabled auto-merge September 10, 2026 20:32
@os-justin
os-justin added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 0aa88eb Sep 10, 2026
38 checks passed
@os-justin
os-justin deleted the claude/issue-16891-publish-manifest-id-loud branch September 10, 2026 20:59
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