Skip to content

feat(pm): add --reconcile to ensure-pm-labels, aligning drifted label objects with the wordbook - #10193

Merged
qq9340100 merged 1 commit into
mainfrom
claude/issue-10117-label-reconcile
Aug 20, 2026
Merged

feat(pm): add --reconcile to ensure-pm-labels, aligning drifted label objects with the wordbook#10193
qq9340100 merged 1 commit into
mainfrom
claude/issue-10117-label-reconcile

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes#10117

Adds an explicit --reconcile mode to scripts/pm/ensure-pm-labels.sh, and carries the one-time per-repo reconciliation command list the maintainer runs to discharge the currently-measured drift.

The change

The default stays create-only. A no-flag run creates the labels this file names where they are absent and leaves every existing label object completely alone — the script's deliberate safety posture, stated in its own comments, preserved byte-for-byte. Verified, not asserted: the 20 gh label create lines are untouched by this diff (git diff shows zero changed vocabulary lines), and a fake-gh capture of a default run produces a call sequence identical to the one captured from the pre-change script.

--reconcile aligns colour and description of the labels the file names, whether or not they already matched. It never deletes a label, never renames one (it never passes --name), and never touches a label the file does not name — retired lane objects and repo-local labels are left exactly as they are. Re-running changes nothing the second time.

Why the mode is a shim over the existing lines rather than a second table

Every label is still written once, in the ordinary gh label create NAME -R REPO -c COLOUR -d "DESCRIPTION" form. A shell function shadows gh and, in reconcile mode, follows each create with the equivalent gh label edit. Two consequences, both load-bearing:

  1. One literal per label. Reconcile re-sends the very string the create line carries, so check:pm-label-desc-cap — which measures the -d text on those create lines — bounds the edited description too. This is what makes the grading note's premise (the desc-cap gate already covers edits) true rather than assumed. A separately-written edit table would drift from the create table and would be measured by nothing.
  2. No line can opt out. Someone adding a label a year from now writes the ordinary create line and gets reconcile behaviour for free. There is no prefix to forget, and no way to add a label that reconcile then silently ignores.

Two failure-shape decisions worth review:

  • An unknown flag is a hard error (exit 2), not a fall-through. A mistyped --reconsile that fell through to the default would print the ordinary success line, and the operator would read a reconciliation that never happened as done.
  • A failed edit is loud and exits non-zero. Creation's || true is mandatory for rerunnability, but the same silence on an edit would report success while leaving the drift live — the exact failure this mode exists to end. Edit stderr is captured rather than inherited, because the call sites send create stderr to /dev/null.

What the measurement actually found

The card named three drifted labels. Measuring every vocabulary label the script names, on the two repos this session can read, found the drift is close to total:

  • objectstack-ai/objectstack: all 21 labels differ. 19 are GitHub's default grey with an empty description — they were auto-created by first application and the create-only script has been a no-op on them ever since. The two exceptions are priority:p0 (correct colour, and the reversed description the card reported) and pm:seat (correct colour, older wording).
  • objectstack-ai/objectui: all 11 differ. 9 grey-and-empty including priority:p0, plus target:v17 carrying a hand-written 2026-08-04 description — and pm:retriage and pm:epic do not exist there at all, which no amount of reconciling fixes but which the same run creates.

The card's framing was that drift is structurally permanent; the measurement says the create-only path has in practice never applied a colour or description to either repo. That strengthens the case for the mode rather than changing it.

One comment in the script was measurably false and is corrected here: the needs:contract-review block claimed its description "matches the live label object". That object is grey with an empty description, so the claim had never been true — nothing had ever checked it.

priority:p0's wordbook entry was checked against the state machine before propagating it, per the dispatch's standing instruction to stop rather than propagate a wrong entry: SKILL.md line 86 reads 插队:可超 batch、破轮次立即派发;⛔ 不豁免同文件串行与认领协议 — ordering, explicitly not an exemption. The wordbook is right and the live object is the reversed one. No wordbook entry was found wrong.

Reconciliation command list (the one-time PM action)

Recommended — one command, all four repos, discharges every drift below including the two repos this session could not read:

bash scripts/pm/ensure-pm-labels.sh --reconcile

It prints one ↻ LABEL @ REPO line per aligned label and exits non-zero if any edit fails.

⚠️objectstack-ai/cloud and objectstack-ai/objectos were NOT measured. This session's GitHub access is limited to objectstack and objectui (Access denied: repository "objectstack-ai/cloud" is not configured for this session), so their live label state is unknown to me. The gh label edit lines given for them below are the wordbook's desired state, which is idempotent regardless of current values — but the drift columns that exist for the other two repos honestly cannot be filled in for these.

The per-repo explicit form follows, for auditing exactly what --reconcile will change before running it. Note the gh label edit lines only align labels that already exist; where a label is absent (objectui's pm:retriage and pm:epic) the script's create path is what supplies it, which is another reason to prefer the single command above.

objectstack-ai/objectstack — 21 labels (drift measured this session)

labellive colourlive descriptiondrift
pm:queueededed(empty)colour + description
pm:dispatchedededed(empty)colour + description
needs-user-decisionededed(empty)colour + description
pm:on-holdededed(empty)colour + description
pm:blockedededed(empty)colour + description
priority:p0B60205Critical: blocker, must ship before MVPdescription
pm:blockingededed(empty)colour + description
pm:retriageededed(empty)colour + description
findingededed(empty)colour + description
pm:epicededed(empty)colour + description
needs:contract-reviewededed(empty)colour + description
repo:objectuiededed(empty)colour + description
repo:cloudededed(empty)colour + description
pm:seat1d76dbPM seat registry issue - single-writer body, index = this …description
domain:engineededed(empty)colour + description
domain:servicesededed(empty)colour + description
domain:devxededed(empty)colour + description
domain:specededed(empty)colour + description
domain:cliededed(empty)colour + description
domain:skillsededed(empty)colour + description
target:v17ededed(empty)description

21 of 21 rows differ from the wordbook.

Explicit gh label edit equivalents (21 commands):

gh label edit pm:queue -R objectstack-ai/objectstack --color 0e8a16 --description 'Ready for the PM dispatch loop'
gh label edit pm:dispatched -R objectstack-ai/objectstack --color 1d76db --description 'Dispatched to a dev agent by /pm-dispatch'
gh label edit needs-user-decision -R objectstack-ai/objectstack --color d93f0b --description 'Blocked on a maintainer decision — do not dispatch'
gh label edit pm:on-hold -R objectstack-ai/objectstack --color e4e669 --description 'Decision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment'
gh label edit pm:blocked -R objectstack-ai/objectstack --color b60205 --description 'Blocked by another issue/PR — body carries Blocked-by: #N'
gh label edit priority:p0 -R objectstack-ai/objectstack --color b60205 --description 'Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial'
gh label edit pm:blocking -R objectstack-ai/objectstack --color 8250df --description 'Derived cache from the Blocked-by reverse index: open card with open dependents — never hand-set'
gh label edit pm:retriage -R objectstack-ai/objectstack --color d4c5f9 --description 'Awaiting triage re-judgement — coexists with the standing pm:* label; queued cards skip dispatch'
gh label edit finding -R objectstack-ai/objectstack --color c2e0c6 --description 'Recorded observation — held, not dispatchable until the findings triage round grades it'
gh label edit pm:epic -R objectstack-ai/objectstack --color 5319e7 --description 'Parent delegated to a dedicated epic PM — other PMs never dispatch into its subtree'
gh label edit needs:contract-review -R objectstack-ai/objectstack --color d93f0b --description 'Clause-② enqueue gate: dispatched below contract-review tier — blocked until the review clears it'
gh label edit repo:objectui -R objectstack-ai/objectstack --color fbca04 --description 'Seam card: cross-repo ordering with objectui is the substance (pure objectui fixes live in objectui)'
gh label edit repo:cloud -R objectstack-ai/objectstack --color c5def5 --description 'Seam card: cross-repo ordering with cloud is the substance (pure cloud fixes live in cloud)'
gh label edit pm:seat -R objectstack-ai/objectstack --color 1d76db --description 'Seat registry post — protocol carrier, not dispatchable work; the label page is the fleet board'
gh label edit domain:engine -R objectstack-ai/objectstack --color bfd4f2 --description 'Domain lane — seat card indexed by label:pm:seat'
gh label edit domain:services -R objectstack-ai/objectstack --color bfd4f2 --description 'Domain lane — seat card indexed by label:pm:seat'
gh label edit domain:devx -R objectstack-ai/objectstack --color bfd4f2 --description 'Domain lane — seat card indexed by label:pm:seat'
gh label edit domain:spec -R objectstack-ai/objectstack --color bfd4f2 --description 'Domain lane — seat card indexed by label:pm:seat'
gh label edit domain:cli -R objectstack-ai/objectstack --color bfd4f2 --description 'Domain lane — seat card indexed by label:pm:seat'
gh label edit domain:skills -R objectstack-ai/objectstack --color bfd4f2 --description 'Domain lane — seat card indexed by label:pm:seat'
gh label edit target:v17 -R objectstack-ai/objectstack --color ededed --description 'Release blocker for v17 — on the board until fixed, dropped as no longer valid, or accepted for GA'

objectstack-ai/objectui — 11 labels (drift measured this session)

labellive colourlive descriptiondrift
pm:queueededed(empty)colour + description
pm:dispatchedededed(empty)colour + description
needs-user-decisionededed(empty)colour + description
pm:on-holdededed(empty)colour + description
pm:blockedededed(empty)colour + description
priority:p0ededed(empty)colour + description
pm:blockingededed(empty)colour + description
pm:retriageabsent — created, not edited
findingededed(empty)colour + description
pm:epicabsent — created, not edited
target:v17edededv17 发布窗口工作集(GA 前排查 2026-08-04)description

11 of 11 rows differ from the wordbook.

Explicit gh label edit equivalents (11 commands):

gh label edit pm:queue -R objectstack-ai/objectui --color 0e8a16 --description 'Ready for the PM dispatch loop'
gh label edit pm:dispatched -R objectstack-ai/objectui --color 1d76db --description 'Dispatched to a dev agent by /pm-dispatch'
gh label edit needs-user-decision -R objectstack-ai/objectui --color d93f0b --description 'Blocked on a maintainer decision — do not dispatch'
gh label edit pm:on-hold -R objectstack-ai/objectui --color e4e669 --description 'Decision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment'
gh label edit pm:blocked -R objectstack-ai/objectui --color b60205 --description 'Blocked by another issue/PR — body carries Blocked-by: #N'
gh label edit priority:p0 -R objectstack-ai/objectui --color b60205 --description 'Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial'
gh label edit pm:blocking -R objectstack-ai/objectui --color 8250df --description 'Derived cache from the Blocked-by reverse index: open card with open dependents — never hand-set'
gh label edit pm:retriage -R objectstack-ai/objectui --color d4c5f9 --description 'Awaiting triage re-judgement — coexists with the standing pm:* label; queued cards skip dispatch'
gh label edit finding -R objectstack-ai/objectui --color c2e0c6 --description 'Recorded observation — held, not dispatchable until the findings triage round grades it'
gh label edit pm:epic -R objectstack-ai/objectui --color 5319e7 --description 'Parent delegated to a dedicated epic PM — other PMs never dispatch into its subtree'
gh label edit target:v17 -R objectstack-ai/objectui --color ededed --description 'Release blocker for v17 — on the board until fixed, dropped as no longer valid, or accepted for GA'

objectstack-ai/cloud — 11 labels (drift NOT measured — see note above)

Explicit gh label edit equivalents (11 commands):

gh label edit pm:queue -R objectstack-ai/cloud --color 0e8a16 --description 'Ready for the PM dispatch loop'
gh label edit pm:dispatched -R objectstack-ai/cloud --color 1d76db --description 'Dispatched to a dev agent by /pm-dispatch'
gh label edit needs-user-decision -R objectstack-ai/cloud --color d93f0b --description 'Blocked on a maintainer decision — do not dispatch'
gh label edit pm:on-hold -R objectstack-ai/cloud --color e4e669 --description 'Decision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment'
gh label edit pm:blocked -R objectstack-ai/cloud --color b60205 --description 'Blocked by another issue/PR — body carries Blocked-by: #N'
gh label edit priority:p0 -R objectstack-ai/cloud --color b60205 --description 'Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial'
gh label edit pm:blocking -R objectstack-ai/cloud --color 8250df --description 'Derived cache from the Blocked-by reverse index: open card with open dependents — never hand-set'
gh label edit pm:retriage -R objectstack-ai/cloud --color d4c5f9 --description 'Awaiting triage re-judgement — coexists with the standing pm:* label; queued cards skip dispatch'
gh label edit finding -R objectstack-ai/cloud --color c2e0c6 --description 'Recorded observation — held, not dispatchable until the findings triage round grades it'
gh label edit pm:epic -R objectstack-ai/cloud --color 5319e7 --description 'Parent delegated to a dedicated epic PM — other PMs never dispatch into its subtree'
gh label edit target:v17 -R objectstack-ai/cloud --color ededed --description 'Release blocker for v17 — on the board until fixed, dropped as no longer valid, or accepted for GA'

objectstack-ai/objectos — 10 labels (drift NOT measured — see note above)

Explicit gh label edit equivalents (10 commands):

gh label edit pm:queue -R objectstack-ai/objectos --color 0e8a16 --description 'Ready for the PM dispatch loop'
gh label edit pm:dispatched -R objectstack-ai/objectos --color 1d76db --description 'Dispatched to a dev agent by /pm-dispatch'
gh label edit needs-user-decision -R objectstack-ai/objectos --color d93f0b --description 'Blocked on a maintainer decision — do not dispatch'
gh label edit pm:on-hold -R objectstack-ai/objectos --color e4e669 --description 'Decision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment'
gh label edit pm:blocked -R objectstack-ai/objectos --color b60205 --description 'Blocked by another issue/PR — body carries Blocked-by: #N'
gh label edit priority:p0 -R objectstack-ai/objectos --color b60205 --description 'Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial'
gh label edit pm:blocking -R objectstack-ai/objectos --color 8250df --description 'Derived cache from the Blocked-by reverse index: open card with open dependents — never hand-set'
gh label edit pm:retriage -R objectstack-ai/objectos --color d4c5f9 --description 'Awaiting triage re-judgement — coexists with the standing pm:* label; queued cards skip dispatch'
gh label edit finding -R objectstack-ai/objectos --color c2e0c6 --description 'Recorded observation — held, not dispatchable until the findings triage round grades it'
gh label edit pm:epic -R objectstack-ai/objectos --color 5319e7 --description 'Parent delegated to a dedicated epic PM — other PMs never dispatch into its subtree'

Verification

Gates re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs (no paths passed — it takes its own changeset from the merge base); the derived set matched the dispatch clue exactly, no additions. All run at final commit d47fba7, each through scripts/pm/os-verify-lock.sh:

  • pnpm check:pm-label-desc-cap✓ check-label-desc-cap --self-test: all cases passed, then ✓ check:pm-label-desc-cap: 16 label descriptions in scripts/pm/ensure-pm-labels.sh, all ≤100 characters (longest: 100, repo:objectui). · os-verify-lock: VERDICT command-exit 0
  • pnpm check:cross-package-test-inputs and node scripts/check-cross-package-test-inputs.mjsAll 60 self-test cases passed. / OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
  • pnpm check:nul-bytescheck-nul-bytes: OK (scanned 6069 text file(s) ...; no raw ASCII control bytes). Plus a direct control-byte scan of both changed files, which matched nothing.

New selftest coverage

14 cases were added to check-label-desc-cap.mjs --self-test, which is the family's CI-wired selftest (lint.yml). They belong there rather than in a new file because the gate's charter is "no description this script sends to GitHub can 422", and reconcile adds a second way to send one: without them the gate would keep passing while silently covering only the create path — the parser-matches-nothing failure the gate was written to prevent, reappearing in a new place. The load-bearing cases assert that reconcile edits exactly the labels it creates with the same name, repo, colour and description; that no verb other than create/edit is ever issued; that no --name reaches gh; and that a second run issues an identical call sequence.

Ablation

The new assertions were checked to be capable of failing. Predicted direction before running: red, specifically the edits-match-creates case and the failing-edit case.

Mutation: the --reconcile arm of the flag parser was changed to set the flag to 0, neutering the mode. Confirmed on disk by grepping for the injected and the deleted text, not by the editor's exit code — the injected form went 0 → 1 occurrences and the deleted form 1 → 0. No build step is involved (both files are read from source at runtime), so there is no dist/ leg to rebuild.

Ablated result, as predicted:

self-test exit=1
✗ reconcile edits exactly the labels it creates — same name, repo, colour and description
✗ a failing gh label edit makes the script exit non-zero
❌ check-label-desc-cap --self-test: 2 case(s) failed

The restore leg was taken from the commit (git checkout of the branch's copy), and confirmed on disk by the same two greps returning to 1 → 0 / 0 → 1, with a clean git status; the gate was then re-run green on the restored tree. Note the third case, "every description reconcile SENDS is one this gate already measured", stayed green under ablation — vacuously, over an empty edit list. That is why the edits-equal-creates case is the load-bearing one and is stated separately.

Scope

skip-changeset: internal PM tooling, nothing user-visible, nothing published.

Not addressed here, recorded for triage rather than fixed in this PR: this session could not read objectstack-ai/cloud or objectstack-ai/objectos, so their drift is unmeasured; and the reconcile mode aligns only labels the file names, so leftover retired lane objects (domain:ui, domain:spec-surface, and the rest listed in the script header) are deliberately still untouched — deleting those remains the separate PM action the header describes.


Generated by Claude Code

@qq9340100qq9340100 added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 20, 2026 — with Claude
@qq9340100
qq9340100 marked this pull request as ready for review August 20, 2026 13:35
@qq9340100
qq9340100 enabled auto-merge August 20, 2026 13:35
@qq9340100
qq9340100 added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit 68f65ffAug 20, 2026
25 checks passed
@qq9340100
qq9340100 deleted the claude/issue-10117-label-reconcile branch August 20, 2026 14:05
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@qq9340100@claude