Skip to content

fix(scripts/pm): give os-verify-lock.sh the index execute bit its own header documents - #19050

Merged
os-elon-musk merged 1 commit into
mainfrom
claude/issue-19044-scripts-pm-sh-exec-bit
Sep 18, 2026
Merged

os-elon-musk merged 1 commit into
mainfrom
claude/issue-19044-scripts-pm-sh-exec-bit

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #19044

Clause-②: no

scripts/pm/os-verify-lock.sh goes 100644 → 100755 in the index. Content is
byte-identical: the blob id is 8ad1813feff415727c4bd16f834707d20c6fc34c before and
after, so the diff adds and removes nothing.

⚠️ Deviation from the dispatched file surface — one file, not three

The dispatch named three modes to flip. The criterion the card asks for first was
measured before any edit, and it places one of them. The two siblings are left at
100644 on purpose.

Their own headers do not document a bare-path invocation. Verbatim, from the usage
blocks:

file its header's usage spelling refused at 100644?
scripts/pm/os-verify-lock.sh scripts/pm/os-verify-lock.sh --status (and 4 more bare-path lines) yes — exit 126
scripts/pm/ensure-pm-labels.sh bash scripts/pm/ensure-pm-labels.sh · bash … --reconcile no — exit 0
scripts/pm/os-regen-merge.sh bash scripts/pm/os-regen-merge.sh · bash … --self-test no — exit 0

So the card's two "controls" widened the mode reading (all three are 100644 — true,
re-measured here) but they do not widen the class-(b) contract violation: there is
exactly one script in this repo whose own documented spelling the tracked mode refuses.
The card itself set the boundary this follows — 「A blanket +x over everything would be
the same unexamined move in the other direction」 — and flipping two files whose documented
spelling was never refused is that move, scoped to one lane.

The criterion, measured

A bare-path invocation SITE is what needs the bit — not a header's prose, and not a
lane's habit. Two kinds of such site exist in this repo, and nothing else executes a
tracked .sh by bare path:

  1. .claude/settings.json hook commands — 5 of them, each spelled as the quoted path with
    no interpreter. All five files are already 100755.
  2. scripts/pm/os-verify-lock.sh's own usage block (5 lines), plus the PM dispatch
    instruction at .claude/skills/pm-dispatch/SKILL.md:438, which spells the full
    bare path scripts/pm/os-verify-lock.sh --status.

Every other execution site spells bash PATH: 17 workflow run: lines (including
lint.yml:1698 and lint.yml:1779, the self-test gates for two of these three scripts)
and 9 package.json script entries. Both .githooks/ references are prose, not calls.

⭐ That second site is the one the triage comment flagged as its source-side upgrade
condition — 「若测到另有文档/模板/派发令在教人用直调拼法」. It exists:
SKILL.md:438 teaches the refused spelling to the PM seat. This PR resolves that
condition rather than escalating it
(the spelling now runs), and the reading is handed
to the seat rather than acted on: priority is not this PR's to move.

Every tracked .sh — mode · shebang · does its header document a bare path

All 29 tracked .sh files carry the shebang #!/usr/bin/env bash. Header = first 40 lines,
self-path in command-word position.

file mode header documents bare path verdict
.claude/hooks/guard-main-checkout.sh 100755 no (invoked bare from settings.json) ok
.claude/hooks/guard-main-checkout-bash.sh 100755 no (invoked bare from settings.json) ok
.claude/hooks/guard-process-kill.sh 100755 no (invoked bare from settings.json) ok
.claude/hooks/guard-shared-stash.sh 100755 no (invoked bare from settings.json) ok
.claude/hooks/guard-tree-enum.sh 100755 no (invoked bare from settings.json) ok
.claude/hooks/guard-main-checkout.selftest.sh 100755 yes (1) ok
.claude/hooks/guard-main-checkout-bash.selftest.sh 100755 yes (1) ok
.claude/hooks/guard-process-kill.selftest.sh 100755 yes (1) ok
.claude/hooks/guard-shared-stash.selftest.sh 100755 yes (1) ok
.claude/hooks/guard-tree-enum.selftest.sh 100755 yes (1) ok
scripts/build-console.sh 100755 yes (1) ok
scripts/bump-objectui.sh 100755 yes (4) ok
scripts/collect-release-notes.sh 100755 yes (4) ok
scripts/bump-objectui.selftest.sh 100755 no (bash ×1) ok
scripts/ci/select-shard-packages.sh 100755 no (bash ×2) ok
scripts/create-scaffold-smoke.sh 100755 no ok
scripts/downstream-smoke.sh 100755 no ok
scripts/gen-sdui-manifest.sh 100755 no ok
scripts/release-publish.sh 100755 no ok
scripts/release-spec-changes.sh 100755 no (bash ×3) ok
scripts/vercel-ignore-docs.sh 100755 no ok
scripts/vercel-ignore-docs.selftest.sh 100755 no (bash ×1) ok
scripts/pm/os-verify-lock.sh 100644 → 100755 yes (5) the one violation — fixed here
scripts/pm/ensure-pm-labels.sh 100644 no (bash ×2) ok — left alone
scripts/pm/os-regen-merge.sh 100644 no (bash ×2) ok — left alone
scripts/ci/select-gate-families.sh 100644 no (bash ×3) ok
scripts/ci/select-gate-families.selftest.sh 100644 no (bash ×1) ok
scripts/ci/select-shard-packages.selftest.sh 100644 no (bash ×1) ok
scripts/publish-smoke.sh 100644 no usage line at all ok

Counts on origin/main at abb01f105: 29 tracked .sh — 22 at 100755, 7 at
100644. (The triage comment's contrast figure of 24 is off by two; the card's and the
dispatch's three scripts/pm/ rows re-measure exactly.)

⇒ After this PR the criterion holds with zero violations across all 29 files, which is
the state shape C would assert if a gate were ever taken. Shape C is not taken here.

Exit codes — before / after, bash PATH as the control both times

Measured in this worktree, branched from origin/main at abb01f105.

invocation before (100644) after (100755)
scripts/pm/os-verify-lock.sh --status 126 (Permission denied) 0
bash scripts/pm/os-verify-lock.sh --status — control 0 0
scripts/pm/os-verify-lock.sh --show-budget 126 0
bash scripts/pm/os-verify-lock.sh --show-budget — control 0 0
scripts/pm/os-verify-lock.sh --self-test 126 0 (all cases pass)
bash scripts/pm/os-verify-lock.sh --self-test — control 0 0 (all cases pass)

And the two files left at 100644, showing the absence of a violation in both directions:

invocation exit
bash scripts/pm/os-regen-merge.sh --help — the documented spelling 0
bash scripts/pm/ensure-pm-labels.sh --help — the documented spelling 0
scripts/pm/os-regen-merge.sh --help — bare path, documented nowhere 126
scripts/pm/ensure-pm-labels.sh --help — bare path, documented nowhere 126

Reader test (one line): a dev copies scripts/pm/os-verify-lock.sh --status out of the
header, pastes it at a worktree root, and it prints the lock state and exits 0 — on this
branch it does; on origin/main it prints Permission denied and exits 126.

The diff

$ git diff --summary abb01f105 HEAD
 mode change 100644 => 100755 scripts/pm/os-verify-lock.sh

$ git diff --stat abb01f105 HEAD
 scripts/pm/os-verify-lock.sh | 0
 1 file changed, 0 insertions(+), 0 deletions(-)

⚠️ One correction to a stated expectation: git diff --stat on a mode-only change is not
empty
— it prints the file with a zero-width graph and 0 insertions(+), 0 deletions(-).
--summary is the line that carries the change.

Mechanism this is built on, measured not assumed

  • No gate or CI job reads a scripts/pm/** file mode. Grepping 100755 / 100644 /
    fileMode / update-index --chmod / X_OK over scripts/**, .github/workflows/**,
    packages/lint/**, .githooks/** and .claude/hooks/** returns mode assertions for
    exactly two paths, both under .githooks/: check-regen-pending.mjs:1615 asserts
    .githooks/pre-push is 100755, and git-merge-regen.mjs:1212 asserts the same for
    .githooks/pre-commit. Both prescribe the fix as git update-index --chmod=+x — this
    repo already treats an index execute bit as an assertable contract, with this exact
    remedy, so shape A's mechanism is the sanctioned one and not a new idea.
  • A mode-only diff is already pinned as clean by the widening-tells gate:
    scripts/pm/check-widening-tells.mjs:4263 pins 「a MODE-ONLY change carries a real 0」 and
    :4280 pins that a mode-only change to a tell surface is clean.
  • Shebangs: all three scripts/pm/*.sh are #!/usr/bin/env bash, so the newly
    executable file runs under bash, not sh. All 29 tracked .sh carry the same shebang.
  • core.fileMode is true in this worktree, so chmod +x plus git add was the
    spelling used; git ls-files -s confirms the index carries 100755, and the worktree
    and index agree (no phantom "mode change back" in git status).
  • Serial safety: the two self-test gates that run these scripts in CI
    (lint.yml:1698, lint.yml:1779) both spell bash PATH, so neither is affected in
    either direction by a mode flip.

Acceptance notes

Observations from the criterion sweep, not filed and not changed here:

  • The 100644 / 100755 split among the files with no bare-path site is arbitrary
    drift, not a rule: scripts/ci/select-shard-packages.sh is 100755 while its sibling
    scripts/ci/select-gate-families.sh is 100644, and both are invoked only as
    bash PATH from lint.yml / ci.yml. Two of the three scripts/ci/*.selftest.sh are
    100644, the scripts/ selftests elsewhere are 100755. Nothing is broken by it and no
    declared contract is crossed — the criterion is one-directional, so a file carrying +x
    it does not need is not a violation.
  • scripts/pm/os-regen-merge.sh:393 spells its --help usage with a bare basename
    (os-regen-merge.sh --self-test), which would not run even with the bit set, since the
    basename is not on PATH. It is a usage label, not a path invocation, so it is outside
    the criterion — noting it as the boundary, not widening the class.
  • scripts/pm/ is 27 × 100644 and 1 × 100755 (release-rehearsal-clone.mjs), so
    「+x is the repo's convention」 is true repo-wide for .sh (22 of 29) and false for this
    lane. The criterion, not the lane habit, is what places the bit.

Changeset

skip-changeset: scripts/pm/** publishes nothing — no package's files[] ships it, and
the diff is a file mode, so no released byte moves. Fast-track path, no measurement needed.


Generated by Claude Code

… header documents

The header's usage block spells five bare-path invocations
(`scripts/pm/os-verify-lock.sh --status`, `-c '<cmd>'`, `-- <argv>`,
`--show-budget`, `--self-test`), and the tracked mode 100644 refused every one
of them with exit 126 / Permission denied. Exit 126 is one digit from the 99 the
same header teaches ("this call never acquired the lock"), and it arrives from a
call that never ran the wrapped command and never printed a VERDICT line, so the
reader has a number the header does not explain.

Mode only: the blob id is unchanged (8ad1813
before and after), so `git diff --summary` is one `mode change 100644 => 100755`
line and the diff adds and removes nothing.

Measured criterion, not a blanket +x: a bare-path invocation SITE is what needs
the bit. The repo has exactly two kinds of such site — the five
`.claude/hooks/guard-*.sh` commands in `.claude/settings.json` (all already
100755) and this script's own usage block plus the PM dispatch instruction at
`.claude/skills/pm-dispatch/SKILL.md:438`. Every other execution site in the
repo (17 workflow `run:` lines, 9 `package.json` scripts) spells `bash <path>`,
so the two sibling `scripts/pm/*.sh` files stay 100644: their headers document
`bash scripts/pm/…` and that spelling was never refused.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
@os-elon-musk os-elon-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e6fd2204a0

① Derived judgments

  • Surface: the tracked mode of scripts/pm/os-verify-lock.sh only — git diff --summary against the merge base abb01f105 is the one line mode change 100644 => 100755, --stat empty, blob 8ad1813fef identical before and after (seat re-read on the fetched head). check-governed-merges.mjs --pr 19050 → NOT governed; no other open PR touches any scripts/pm/*.sh.
  • The criterion first, as the card asked — and it corrects the dispatch. The seat's dispatch said the three scripts' headers document a bare-path invocation 「and the twins in the other two headers」; the dev measured that there are no twins: ensure-pm-labels.sh documents bash scripts/pm/ensure-pm-labels.sh and os-regen-merge.sh documents bash scripts/pm/os-regen-merge.sh, both exit 0 at 100644 in their documented spelling (and 126 in the undocumented bare one). The card had used those two as MODE controls (all three 100644 — true), never as contract carriers; the dispatch read a control as a contract — the seat's error, recorded. The criterion the dev pinned is the right one: a bare-path invocation SITE is what needs the execute bit, and the repo has two kinds of site — the five .claude/settings.json hook commands (all already 100755) and this script's own five-line usage block plus the dispatch instruction at SKILL.md :438; every other execution site (17 workflow run: lines including lint.yml's self-test gates for two of these scripts, 9 package.json entries) spells bash PATH. Measured over all 29 tracked .sh (22 at 100755, 7 at 100644, all with #!/usr/bin/env bash): nine document a bare path, of which os-verify-lock.sh was the only one at 100644 — one violation, now zero. Shape A on the one file the criterion places; B (header rewrite) and C (a gate) correctly not taken. The triage comment's contrast figure (24 tracked 100755) is 22 by both git ls-tree -r and git ls-files -s — the seat's own 22 / 7 reading agrees; noted for triage's readers, not a finding.
  • The premise re-run first, as triage asked: fresh worktree at abb01f105, direct scripts/pm/os-verify-lock.sh --status exit 126, bash … exit 0, index mode 100644 — the class (b) violation stands. After the fix all five documented spellings run directly (--status 0, --show-budget 0, --self-test 0 「all cases pass」, -c '…' 0 with a VERDICT line, -- … 0), bash PATH the control on each, 0 both times; before the fix all five returned 126.
  • Seat re-measure on the fetched head: the summary line and the blob identity above; --pair 19050 exit 0; closingKeywordTargets = {19044 → Fixes}; the newest (only) Claim: (5731178252) names this branch; git ls-tree on the head: 100755 … os-verify-lock.sh, the other two unchanged at 100644.

② Semver level

  • skip-changeset is correct (scripts/pm/** publishes nothing; applied by label-write.mjs, read back). Clause-②: no on the claim and in the body; --pair 19050 at 2026-09-18T14:34Z: exit 0, no widening tell.

③ Boundary flags

  • Dev flags: no open questions. Deviations: the dispatch's 「twins」 premise refuted by measurement (①, the seat's); ① / ② empty by construction (no package touched), declared rather than reported green; no classifier denial. Out-of-scope readings carried by the seat: the six other 100644 .sh files have no bare-path site anywhere (no card owed); the triage count off by two (a note on the card thread suffices, below).
  • Gates: 23 derived / 23 run / 0 NOT-MEASURED at e6fd2204a (every exit 0; re-derived after origin/main moved to 67624b78d, identical) + three artifact-roster gates the derivation flagged (check:select-gate-families 40 cases — its case statement maps this script to the verify-lock family; check:pm-label-desc-cap; check:select-shard-packages), all 0; repo-wide eslint proven not owed (the path is outside eslint's population and the blob is unchanged). 0 MCP calls; writes as budgeted.
  • CI on this head at 2026-09-18T14:34Z: 18 success · 10 skipped · 2 in progress — nothing red; the landing act reads it again.
  • Landing: NOT governed ⇒ the queue on this seat's record through the CCR route (the channel is open under the maintainer's manual confirmation since 14:25Z): ready + auto-merge SQUASH once the two in-progress checks complete.

Implemented-by: claude/issue-19044-scripts-pm-sh-exec-bit
Reviewed-by: session_01BTeBejoPUvRHN8WdAJC6oF

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Landing record — landed on the seat's record through the CCR route (skills seat, session_01BTeBejoPUvRHN8WdAJC6oF) · 2026-09-18T14:41Z

Channel: the allow rules of PR #19047 on disk under the maintainer's manual-confirmation mode (open since 2026-09-18T14:25Z); the act typed in the rules' literal-prefix shape. Pre-checks: NOT governed (scripts/pm/os-verify-lock.sh, mode-only); --pair 19050 exit 0; the review of record is 5731545537 (PASS) on the head e6fd2204a0; CI on the head 21 success · 10 rostered skips · 0 red at 2026-09-18T14:40Z; mergeable_state clean; ACCEPT 5731550965 on #19044.

Acts: POST …/pulls/19050/ccr/ready_for_review HTTP 200 at 2026-09-18T14:40Z (timeline ready_for_review, draft: false read back); PUT …/pulls/19050/ccr/auto_merge {"merge_method":"SQUASH"} HTTP 200 at 2026-09-18T14:41Z (timeline auto_merge_enabled; the repository allows squash only, so the queue's merge is a squash); armed and waiting for the queue's admission (timeline auto_merge_enabled; added_to_merge_queue follows when the queue admits it). The queue rebuilds the PR onto the current main and re-runs the subscribing workflows; the merge is the queue's, ⛔ not this seat's click. Residue on the merge: #19044 → pm:done, unassigned.


Generated by Claude Code

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

Labels

size/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants