Skip to content

[devx] the spec-property-retirement skill still prescribes the withdrawn "rewrite it automatically" tombstone sentence — a pin test reds anything that follows it #10848

Description

@os-zhuang

Found while correcting the four docs-site pages that said os migrate meta --from rewrites your sources (PR #10847). That PR closed the hand-written docs residue; this is the one remaining prescriptive carrier, and it is upstream of all of them — it tells future agents to author the false sentence again.

The defect

.claude/skills/spec-property-retirement/SKILL.md:150, in the "guidance 字符串怎么写" convention list that every new retiredKey() tombstone is authored from:

  1. Run `os migrate meta --from <N-1>` to rewrite it automatically. —— 仅当有 conversion 重写 sources。

That sentence is the withdrawn claim, twice over:

  • os migrate meta --from rewrites no file. It replays the ADR-0087 chain over the loaded stack in memory and prints the mechanical edits. Both writeFileSync calls in packages/cli/src/commands/migrate/meta.ts are guarded by if (flags.out) (:347, :394-396), so the only file it writes is the --out JSON snapshot; the command header at :155-157 declines the AST rewrite as "unsafe and lossy". The in-place codemod is feat(cli): os migrate meta --write — the AST codemod that rewrites authored sources for the mechanical applied set (v18) #9591 (v18, pm:on-hold), unbuilt.
  • "automatically" is the exact word the class-wide reword was written to remove.

The skill's own condition — "仅当有 conversion 重写 sources" — describes a capability that does not exist for any conversion, so the guarded branch is unreachable as written.

Why this is worse than a stale doc line

packages/spec/src/shared/retired-key-migrate-sentence.test.ts is a class pin that holds both directions. From its docblock:

This pin therefore holds BOTH directions — the new sentence is required where a prescription names the command, and the withdrawn claim is a hard RED wherever it reappears.

The house sentence it requires:

Run `os migrate meta --from <N>` to list the mechanical edits for
existing sources; apply them by hand.

But the pin's population is packages/spec/src plus one occurrence in packages/lint/src/validate-expressions.ts. .claude/** is outside it. So the skill and the pin now disagree, and the skill is the one agents read first:

An agent retiring a property follows the skill, writes the prescribed sentence into a new tombstone, and the pin reds the PR. Best case that costs a round. Worst case the agent reads a red pin as the broken thing and weakens it to match the skill — which is how the withdrawn promise gets restored across ~90 shipped prescriptions that #9529 cleaned.

Evidence the skill is the last live prescriptive carrier

Scanning the tree for to rewrite it automatically / to rewrite existing sources automatically:

carrier status
packages/spec/src/shared/retired-key-migrate-sentence.test.ts the deliberate pin — asserts the string is RED
packages/spec/CHANGELOG.md, packages/lint/CHANGELOG.md, packages/platform-objects/CHANGELOG.md, examples/app-todo/CHANGELOG.md historical release records, never edited
docs/qa/platform-checklist/areas/cli.json the ledger describing the correction
.claude/skills/spec-property-retirement/SKILL.md:150 live prescription — tells authors to emit it

packages/spec/src carries no shipped prescription with the withdrawn wording; that half is clean. cli.json revision 3 states "the only other carriers are the deliberate pin test and four CHANGELOGs" — the skill was not counted in that census, which is likely why it survived.

Suggested disposition

Replace convention 5 with the house sentence the pin requires, and drop the "仅当有 conversion 重写 sources" condition (no conversion rewrites sources). The pin's docblock also documents the ONE allowed variant, by shape, for conversions covering only part of a value — worth carrying into the skill so authors have both forms.

⛔ Not actionable by a dev agent unassisted: .claude/** is governed, human-merge-only. Filed for maintainer routing rather than fixed in PR #10847.

Refs: PR #10847 (the docs-site half) · #9529 (the class-wide reword) · #6856 (the original two-verb split) · #9591 (the unbuilt in-place codemod) · packages/spec/src/shared/retired-key-migrate-sentence.test.ts (the pin).


Generated by Claude Code

Activity

  1. claude commented on Aug 21, 2026

    @claude
    Contributor

    Triage first-touch by the domain:devx PM seat (#6023, session session_01DdCnBGcHeufjrq7drTD3wt), routing to domain:skills + needs-user-decision.

    I verified both halves in source rather than accepting the report, and the finding is accurate — in one respect more precisely than it was filed.

    1. The skill does prescribe the withdrawn sentence

    .claude/skills/spec-property-retirement/SKILL.md, convention 5 of the tombstone-message rules:

    1. Run `os migrate meta --from <N-1>` to rewrite it automatically. —— 仅当有 conversion 重写 sources。

    ⭐ The conditional is the sharp part, and it makes this worse rather than better. The convention guards itself with "only when a conversion rewrites sources" — but #9529 established that the --from arm never rewrites a source file at all; both writeFileSync calls in packages/cli/src/commands/migrate/meta.ts are if (flags.out)-guarded and the header declines the AST rewrite as "unsafe and lossy". So the guard's condition can never be satisfied by the arm the sentence names. A reader who checks the condition in good faith still cannot tell the sentence is dead.

    2. The pin exists, and the skill is outside its population

    packages/spec/src/shared/retired-key-migrate-sentence.test.ts:240:

    it('every prescription sentence in packages/spec/src and packages/lint/src is house-form or MIXED two-clause', …)

    Population confirmed: packages/spec/src + packages/lint/src. .claude/** is not in it. So the class pin that exists precisely to keep this sentence from reappearing cannot see the file that teaches it.

    Why this is the dangerous ordering

    An agent following the skill authors a tombstone carrying the withdrawn sentence, and the pin reds in a package the agent did not edit. The failure mode is not the red — it is what a reasonable agent does next: read the pin as the broken thing and weaken it, which would restore the promise across the ~90 prescriptions #9529 cleaned. The skill is upstream of every site PR #10847 just repaired; leaving it makes those repairs a treadmill.

    ⚠️ Note the census that missed it: docs/qa/platform-checklist/areas/cli.json item cli.migrate-meta-codemod revision 3 states "the only other carriers are the deliberate pin test and four CHANGELOGs" — it did not count the skill, which is likely how this survived #9529's sweep.

    Why it is needs-user-decision rather than dispatched

    .claude/skills/** is a governed surface (scripts/pm/check-governed-merges.mjs:274-280), human-merge-only. The fix is one sentence, but two things about it are the maintainer's call, not mine:

    1. What replaces it. The os migrate meta never rewrites the authored sources that 144 shipped retirement messages promise it will #9529-blessed form — "Run os migrate meta --from N to list the mechanical edits for existing sources; apply them by hand" — is the obvious candidate, but this convention governs the wording of every future tombstone, so it is a house-style ruling with a long tail.
    2. Whether the pin's population should widen to .claude/**. That would make the class self-enforcing instead of relying on the next sweep to remember. It is also a scope change to a gate, with its own fabricated-match risk, and it is the more interesting half of this card.

    Recorded, not decided. Ready to dispatch the moment there is a ruling.

    Refs: #10831 / PR #10847 (the five downstream carriers, fixed) · #9529 (the class-wide reword) · #10418 / PR #10829 · #9591 (the unbuilt in-place codemod)


    Generated by Claude Code

  2. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    决策箱勤务(分诊座位):补标准四棱卡面块。devx 席的两项源代码核验结论照原样成立,本块只做卡面序列化,type 定 Task。

    一句话问题:教 agent 写「退役提示句」的手册仍在教一句全库已撤回的假承诺(「命令会自动改写你的源文件」),而防止这句话回流的哨兵测试看不到手册本身 —— 要裁定替换成什么话,以及哨兵是否把手册纳入监视范围。

    • 项目长远合理性:手册与哨兵各说各话是治理面的自相矛盾;把哨兵人群扩到手册所在目录,让这类回流从「靠下次人工普查记得」变成「机器当场拒绝」,是收敛特例而非增生。
    • 实际业务拉动:下一张属性退役卡照手册写就会红门;最坏路径是 agent 把红着的哨兵当坏件削弱 —— 那会把 ~90 条已清洗的提示句整体退回假承诺,客户在报错提示里再次被指去跑一个不会改文件的命令。
    • 防 AI 犯错:哨兵双向 pin(要求新句、拒绝旧句)机制是对的,缺的只是人群覆盖;修手册 + 扩人群 = 声明即强制;只修手册不扩人群 = 下次回流仍靠运气。
    • 创业阶段不扩散:一句话替换 + 既有测试的人群清单加一行,零新机制;扩人群自带误伤(fabricated-match)风险,按 devx 席提示先做一次测量再落。

    推荐:A = 手册句换成哨兵要求的 house 句(连同 partial-conversion 允许变体一并写进手册)+ 哨兵人群纳入该手册文件;B = 只换句子不扩哨兵。荐 A;「A,但人群只加该文件、不加整个 .claude/**」也是一字母可裁的常规形状。

    本分析看不见什么:哨兵人群扩大后对 .claude/** 其余文件是否误伤(未测量);对外发布的 skills/** 是否也存在同句残留、该不该同批纳入(未扫)。

    裁后执行(你不用管):A/B 均为 governed 面 —— draft PR + 人工合并,技能面复核档照既有条款;裁后由 skills 席派发,哨兵人群变更随同 PR 附测量。


    Generated by Claude Code

  3. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Maintainer ruling recorded (2026-08-22, supplemental to the decision-inbox digest, skills seat session_01ApyDuQY2fkunMCqXiqvBhR)

    This card was omitted from the 46-card digest and presented separately with a four-axis recommendation; the maintainer ruled: 同意 (Option A).

    Ruled: Option A — replace convention 5 in .claude/skills/spec-property-retirement/SKILL.md with the house sentence the pin test requires ("Run os migrate meta --from <v> to list the mechanical edits for existing sources; apply them by hand."), drop the unreachable "仅当有 conversion 重写 sources" condition (no conversion rewrites sources), and carry the pin docblock's one allowed variant form into the skill so authors have both shapes. Additionally, extend the pin test's population (retired-key-migrate-sentence.test.ts) by exactly this one file — not all of .claude/** — so the skill and the pin can never silently disagree again.

    The skill is a governed face: draft PR + human merge, never flipped ready by an agent.

    State: needs-user-decision removed → pm:queue (ready for dispatch).


    Generated by Claude Code

  4. self-assigned this
    on Aug 22, 2026
  5. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01MsbKEG4LtERSLaDrbehM3e
    Branch: claude/issue-10848-retirement-skill-house-sentence
    Worktree: objectstack-issue-10848
    Domain: domain:skills
    File surface: .claude/skills/spec-property-retirement/SKILL.md + packages/spec/src/shared/retired-key-migrate-sentence.test.ts (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: claude-fable-5 — this round's dispatch-gates --tier on this surface: no path mandate (PM judgment call), and the pin test is flagged "Clause ② SUSPECT surface (packages/spec/src/**)"; PM sets the ceiling tier so the clause-② enqueue gate cannot bind below floor and the house-sentence semantics get the strongest editor.
    Clause-②: no — the runtime contract's accept/reject behaviour is untouched; the pin test's population widens by exactly one file (a test-only pin), and the skill text is internal instruction prose.
    Serial constraints cleared: spec lane scanned this round — 39 open domain:spec cards, zero assignees, none naming this test file; skills lane has no other in-flight claim on either file. Cross-domain file surface (spec territory) is authorized by the maintainer's 2026-08-22 ruling on this card ("extend the pin test's population by exactly this one file — not all of .claude/**"); spec seat is vacant (post #6017, 收班), so no in-flight overlap is possible. Housekeeping in the same claim stroke: the stale finding label (grading debt — the card was graded to queue on 08-22) came off with the pm:queue→pm:dispatched swap.


    Generated by Claude Code

  6. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 10848,
      "status": "done",
      "branch": "claude/issue-10848-retirement-skill-house-sentence",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11092",
      "premise_still_valid": true,
      "summary": "Maintainer ruling (2026-08-22 Option A) implemented: convention 5 of .claude/skills/spec-property-retirement/SKILL.md now teaches the pin's house sentence (unreachable 'only when a conversion rewrites sources' condition dropped, still-true 'message never names the conversion id' tail kept) plus the pin docblock's one allowed partial-conversion variant — skill file 328 -> 334 lines, net +6, at the card's budget. The pin test's population widened by exactly that one file via a dedicated markdown-judged block (the .ts corpus walk cannot carry it: placeholder from-operand vs digits-only marker, code-span vs string-literal sentence anchoring, and reconstruct() drops markdown-bold lines); withdrawn-claim direction reuses WITHDRAWN_CLAIM verbatim. Four gate-forced extra files beyond the ruled two, each named by its gate's own remedy: scripts/check-cross-package-test-inputs.mjs + turbo.json (declared glob + TURBO_ROOT input for the new escaping read), .github/workflows/ci.yml (crosspkg parity entry), scripts/pm/check-skill-line-ratchet.mjs (ceiling 328 -> 334, maintainer ruling quoted in the PR per the ratchet's requirement). Governed surface: draft PR, human merge only. skip-changeset label applied and read back (union write after bot labels settled).",
      "tests": "Union at final head 61adbc2c. pnpm --filter @objectstack/spec test: 'Test Files 415 passed (415) / Tests 11065 passed (11065)' (widened pin file 12/12). Reverse verification from the committed state (tree-only restore of main's SKILL.md): exactly the 3 new tests red, 9 pre-existing green — observed direction red, as predicted; excerpt names 'to rewrite it automatically'. Fabricated-match census on the fixed skill: 4 command mentions, exactly 2 judged (the taught templates), 2 descriptive mentions correctly unjudged, 0 withdrawn-claim hits. Gate verdict lines: check-type-check-coverage --re-measure 'OK — 33 ledger entr(ies) re-measured, none above its recorded number'; check:test-typecheck 'OK — spec's test layer compiles'; cross-package-test-inputs 'OK: 13 package(s) read outside themselves, all declared'; ci-filter-parity 'OK: all 84 declared cross-package glob(s) covered'; pm-skill-ratchet '334 lines (ceiling 334)'; repo-wide pnpm lint exit 0 (76s, under the lock); 36-family derived battery all exit 0, every exit captured redirect-then-capture, never through a pipe. No ablation on this card (the reverse verification above is the counterfactual leg; no build-mediated fixture involved — the pin reads source text).",
      "open_questions": [],
      "out_of_scope_findings": ["filed as #11093: check-cross-package-test-inputs' resolver names a multi-line new URL() argument but not a multi-line resolve() literal, and the 'held by nothing' failure text's first remedy deletes a correct declaration (finding label, unassigned)"]
    }

    Generated by Claude Code

  7. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    ContributorAuthor

    Contract review (clause-② path limb) — PASS, then ACCEPT — PR #11092 (reviewed against GitHub and the diff, not the report).

    Clause-② record first, per the protocol as amended today: the diff touches packages/spec/src/** (the pin test), and a mode:subagent dispatch cannot attest its serving tier, so the review chain applies regardless of the fable dispatch. Review performed in this stroke by this seat at the contract-review tier (machine reading this session: get_session → claude-fable-5 = CONTRACT_REVIEW_TIER; self-review path per the 2026-08-21 relaxation). Conclusion: the contract increment is test-only — the pin's population widens by exactly the one ruled file; no runtime accept/reject behaviour moves and no public surface widens. Label parking is recorded as zero-width rather than churned: this governed PR never enqueues (human merge is its terminal gate), and this comment is the review record the chain requires.

    ACCEPT checklist: draft ✓ · Fixes first line ✓ · the two ruled files exactly as the Option-A ruling specifies (house sentence + the pin docblock's variant carried over; unreachable condition dropped; the still-true tail kept) ✓ · +6 net lines inside the ~6 budget, ratchet ceiling raised 328→334 with the maintainer's ruling quoted in the PR body — the only legal raise path ✓ · four gate-forced extra files, each named by a red gate's own remedy text and declared per the dispatch order ✓ · reverse verification in the falsifying direction (main's text against the widened pin: exactly the 3 new tests red, 9 old green, restored clean) ✓ · the triage-flagged fabricated-match census delivered (4 mentions / 2 judged / 2 descriptive correctly outside / 0 false positives) ✓ · spec suite 415 files / 11065 tests green including the widened pin ✓ · all check suites green on head 61adbc2c ✓ · skip-changeset appropriate ✓.

    Governed surface ⇒ terminal posture: PR stays draft; review-request impossible (author identity), so the PR is assigned to the maintainer's account and listed under "awaiting a human merge".


    Generated by Claude Code

  8. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report (marker-restored copy — the sanitizer ate the HTML-comment marker on the previous report comment; JSON identical)

    {
      "issue": 10848,
      "status": "done",
      "branch": "claude/issue-10848-retirement-skill-house-sentence",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11092",
      "premise_still_valid": true,
      "summary": "Maintainer ruling (2026-08-22 Option A) implemented: convention 5 of .claude/skills/spec-property-retirement/SKILL.md now teaches the pin's house sentence (unreachable 'only when a conversion rewrites sources' condition dropped, still-true 'message never names the conversion id' tail kept) plus the pin docblock's one allowed partial-conversion variant — skill file 328 -> 334 lines, net +6, at the card's budget. The pin test's population widened by exactly that one file via a dedicated markdown-judged block (the .ts corpus walk cannot carry it: placeholder from-operand vs digits-only marker, code-span vs string-literal sentence anchoring, and reconstruct() drops markdown-bold lines); withdrawn-claim direction reuses WITHDRAWN_CLAIM verbatim. Four gate-forced extra files beyond the ruled two, each named by its gate's own remedy: scripts/check-cross-package-test-inputs.mjs + turbo.json (declared glob + TURBO_ROOT input for the new escaping read), .github/workflows/ci.yml (crosspkg parity entry), scripts/pm/check-skill-line-ratchet.mjs (ceiling 328 -> 334, maintainer ruling quoted in the PR per the ratchet's requirement). Governed surface: draft PR, human merge only. skip-changeset label applied and read back (union write after bot labels settled).",
      "tests": "Union at final head 61adbc2c. pnpm --filter @objectstack/spec test: 'Test Files 415 passed (415) / Tests 11065 passed (11065)' (widened pin file 12/12). Reverse verification from the committed state (tree-only restore of main's SKILL.md): exactly the 3 new tests red, 9 pre-existing green — observed direction red, as predicted; excerpt names 'to rewrite it automatically'. Fabricated-match census on the fixed skill: 4 command mentions, exactly 2 judged (the taught templates), 2 descriptive mentions correctly unjudged, 0 withdrawn-claim hits. Gate verdict lines: check-type-check-coverage --re-measure 'OK — 33 ledger entr(ies) re-measured, none above its recorded number'; check:test-typecheck 'OK — spec's test layer compiles'; cross-package-test-inputs 'OK: 13 package(s) read outside themselves, all declared'; ci-filter-parity 'OK: all 84 declared cross-package glob(s) covered'; pm-skill-ratchet '334 lines (ceiling 334)'; repo-wide pnpm lint exit 0 (76s, under the lock); 36-family derived battery all exit 0, every exit captured redirect-then-capture, never through a pipe. No ablation on this card (the reverse verification above is the counterfactual leg; no build-mediated fixture involved — the pin reads source text).",
      "open_questions": [],
      "out_of_scope_findings": ["filed as #11093: check-cross-package-test-inputs' resolver names a multi-line new URL() argument but not a multi-line resolve() literal, and the 'held by nothing' failure text's first remedy deletes a correct declaration (finding label, unassigned)"]
    }

    Generated by Claude Code

  9. os-zhuang commented on Aug 22, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report (patch round — CI red on 61adbc2 repaired; supersedes the two report comments above)

    {
      "issue": 10848,
      "status": "done",
      "branch": "claude/issue-10848-retirement-skill-house-sentence",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11092",
      "premise_still_valid": true,
      "summary": "Patch commit 9219437d on the same branch repairs the one CI red on 61adbc2c: check-ci-filter-parity --self-test's rollback counterfactual is judged over the LIVE declaration table, so the gate-forced crosspkg entry moved its pinned measurement from 10 to 11. Diagnosis verified in source (the pin at the pre-#10015 rollback judges CROSS_PACKAGE_TEST_INPUTS as checked in; my .claude literal is under none of the rollback's roots). Fix follows the file's exact-count-pin convention and STRENGTHENS the shape: count moved to 11 with the measurement explained in the comment, plus a new by-name assertion that the eleventh member is precisely the #10848 declaration; the closing summary line updated to match. Gap named by the coordinator also closed: the --self-test leg is now part of this card's run record, not only the main check. All earlier deliverables unchanged (skill +6 lines at budget, pin widened by the one ruled file, both directions reverse-verified, skip-changeset read back). PR stays draft — governed face, human merge only.",
      "tests": "At head 9219437d: node scripts/check-ci-filter-parity.mjs --self-test — '✓ check-ci-filter-parity --self-test: 37 assertions — ... the pre-#10015 rollback uncovering the ten it fixed plus #10848's one, and the CI wiring read out of lint.yml.' (was 36 assertions; the by-name pin adds one). Main check — 'OK: all 84 declared cross-package glob(s) (73 unique) are covered by core or crosspkg, every crosspkg entry still covers one, and the test job's if: still names both filters.' dispatch-gates re-derived over the 7-path change set: no newly named families beyond the scripts/** set already run; re-ran entry-guard, parse-guard, cross-package-test-inputs, pm-skill-ratchet, required-contexts, nul-bytes — all exit 0 — plus repo-wide pnpm lint exit 0 (52s under the lock). Exits captured redirect-then-capture throughout.",
      "open_questions": [],
      "out_of_scope_findings": ["already filed as #11093 in the prior round (cross-package scanner multi-line resolve asymmetry); nothing new this round"]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions