Skip to content

docs(skills): the platform skill counts the eight generator barrels, picklists included - #21337

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-21018-skill-generator-barrels
Oct 2, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-21018-skill-generator-barrels

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21018
Clause-②: no

This PR is item 4 of #21018, the last item on the card: the Tier H line in skills/objectstack-platform/SKILL.md that counts and lists the blank starter's generator barrels. The code half landed on main as bcd68a29f3 (PR #21167), and since that landing the sentence has been false. The claim for this PR is 5945859013 on the card; the dev session is session_01VvcEokUG1tvVxkceYfR5XB.

Premise, checked on origin/main (1caa603730, then merged 222ecc27f9)

  • packages/create-objectstack/src/templates/blank/objectstack.config.ts imports eight barrels and hands each to its stack key, in this order: objects, views, actions, flows, dashboards, apps, skills, picklists. src/ of the template holds the same eight directories.
  • os init derives its wiring from the roster: SCAFFOLD_WIRED_BARRELS (packages/cli/src/commands/init.ts:602) maps GENERATOR_SCAFFOLD_TARGETS, which is Object.entries(GENERATORS) in generate.ts — eight generators: object, view, action, flow, dashboard, app, skill, picklist.
  • packages/cli/test/create-objectstack-wiring-parity.test.ts holds the blank template's import lines and stack-key lines equal to the os init rendering, so the template list and the roster cannot drift apart.
  • So the true sentence is "the eight generator barrels", with picklists last — the order the template file uses. The SKILL.md line said seven and listed seven.

What changed

One file, two lines, net zero lines: skills/objectstack-platform/SKILL.md:194-195.

Before:

  generic connector executors in `plugins:`; and the seven generator barrels
  (`objects`, `views`, `actions`, `flows`, `dashboards`, `apps`, `skills`),

After:

  generic connector executors in `plugins:`; and the eight generator barrels
  (`objects`, `views`, `actions`, `flows`, `dashboards`, `apps`, `skills`, `picklists`),

This is the text PR #21167's "Not in this PR" section proposed, byte for byte.

The skills/** readings (both halves)

Reading Before (1caa603730) After (82e06107cf) Delta
skills/objectstack-platform/SKILL.md, lines 489 489 0
skills/objectstack-platform/SKILL.md, tokens (ceil(utf8 bytes / 4), the ratchet's convention) 5827 5830 +3 (ceiling 5833, headroom 3)
Whole published catalog, all 10 skills/**/SKILL.md, lines 4397 4397 0
Whole published catalog, all 10 skills/**/SKILL.md, tokens 52132 52135 +3

The 13 added bytes are the word eight for seven (same length) plus , \picklists`. No re-wrap, no content removed, no ceiling moved. node scripts/check-skills-token-ratchet.mjsreadsskills/objectstack-platform/SKILL.md is 5830 tokens (ceiling 5833; headroom 3)on82e0610`.

The sweep of skills/**

Every file under skills/ was grepped for a count word (six to nine) near barrel / generator / template / scaffold, for the word barrel, for picklist, and for src/ directory listings. Only SKILL.md:194-195 states a count or list that PR #21167 made false. The other hits, each left alone:

  • skills/objectstack-platform/SKILL.md:210-235, the "Project Structure Conventions" tree: a generic convention listing (objects, views, apps, flows, actions, dashboards, reports, datasets, i18n, handlers, each marked optional). It never enumerated the template's barrels (it omitted skills already), so it states no count or list that is now wrong.
  • skills/objectstack-platform/references/bootstrap.md:55-72 and :163-180: illustrative config examples with four barrels and one barrel. Examples, not a roster.
  • skills/objectstack-platform/evals/config-plugins-ops.json:7: an eval's expected output for a CRM config ("barrel imports … same for views / flows"). Not a roster.
  • skills/objectstack-platform/SKILL.md:44-45: the stack-key list already names picklists and picklistExtensions. Correct.

Changeset

None, declared with the skip-changeset label. The diff publishes nothing from any released package: no package.json under packages/ or apps/ names a skills path in its files[] (positive control: @objectstack/spec's files[] lists dist, json-schema, …), and create-objectstack installs the catalog at scaffold time with npx skills add objectstack-ai/objectstack/skills (packages/create-objectstack/src/skills-install.ts), reading this repository directly rather than a bundled copy.

Gates

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run with no paths, derived 25 families from the change set (1 path). The list is identical before and after the origin/main merge.
  • All 25 were run on the final head 82e06107cf, each exit code captured before any pipe: 25 exit 0. --ran reconciliation: ✓ dispatch-gates --ran: 25 derived famil(ies) accounted for — 25 run, 0 NOT-MEASURED.
  • On the pre-merge head 84fc471b7f the same 25 were run once before: 24 exit 0 and one PREREQUISITE NOT MET (exit 3, @objectstack/lint check:doc-formula-expressions, the package was not built). After pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint under the verify lock it exited 0; the merged-head run above includes it green.
  • Also attempted, outside the derived 25: pnpm --filter @objectstack/spec run check:skill-examples. It refused with PREREQUISITE NOT MET (exit 3: packages/client-react/dist holds no declarations). NOT MEASURED locally; it is declared to CI. The diff changes no ts/tsx fence, which is the only surface that gate reads.
  • scripts/pm/check-skill-line-ratchet.mjs is not applicable: its header excludes the published skills/ catalog by design; the token ratchet above is the catalog's gate.
  • pnpm lint is CI's run; this diff touches one Markdown file, which is outside eslint's population (eslint.config.mjs lints ts/tsx/js/mjs), so nothing here moves a lint verdict.

Acceptance notes

  • skills/objectstack-platform/references/operations.md:26 reads "os generate KIND | Scaffold an object / view / flow / agent from a template" (KIND spelled there as a placeholder in angle brackets), while skills/objectstack-ai/SKILL.md:344 says os g agent is retired. Pre-existing, not a count or list PR feat(cli,create-objectstack): os generate picklist, the src/picklists starter barrel, and a Picklists count in the metadata summary #21167 touched, and outside this claim's purpose; noted, not filed.
  • The "Project Structure Conventions" tree in the same SKILL.md (see the sweep) lists neither skills/ nor picklists/. It is a generic convention list with no count, so it is not false; noted for a future prose pass, not changed here.
  • The branch carries one merge commit of origin/main (222ecc27f9, two commits touching scripts/pm/fleet-write/* and scripts/pm/issue-*.mjs, none touching skills/ or a gate this diff derives). The PR's net diff against origin/main is the one file, +2 / −2.
  • Tier H: this PR stays a draft; it lands only after an authorized approval is on record, through the owning seat.

维护者速读(草稿)

改了什么

平台技能包 skills/objectstack-platform/SKILL.md 里描述 blank 模板的那一句:把「七个生成器目录」改成「八个」,并在列表末尾补上 picklists。改两行、删两行,净零行;整个技能包行数不变(4397 行),token 读数 5827 → 5830(上限 5833,余量 3)。

为什么改

上一个 PR(代码半,#21167)落地后,npm create objectstack 新建的项目实际接了 8 个目录,多出的是 src/picklists(共享选项列表,os generate picklist 的产物)。技能文本还写 7 个:AI 读了会少认一个目录,不知道新生成的选项列表落在哪里、挂在哪个 stack 键下。

风险与代价(含回滚)

只改一句说明文字,不改任何代码,不发任何 npm 包。技能目录由 npx skills add 从仓库直接拉取,所以合并后新建的项目立刻读到新句子;已建项目不受影响。回滚就是 revert 这一个 commit。本地派生的 25 个门禁全绿;skills/** 的 token 棘轮没动上限。

席位意见

(留空,席位定稿时填写)

你要做的

在本 PR 上给一个 Approve(skills/** 是受管面 Tier H,需要维护者的批准记录);之后由席位负责落地,不用你再操作。


Generated by Claude Code

claude added 2 commits October 2, 2026 05:01
…picklists included

The blank starter wires eight barrels since the picklist generator landed
(`src/picklists` under the `picklists` stack key). The template sentence
in skills/objectstack-platform/SKILL.md still said seven and listed seven;
it now says eight and names `picklists`, net zero lines.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xs documentation Improvements or additions to documentation labels Oct 2, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 82e06107cf542466921d3f9ecd993d4ece712727
Local-runs: none

Inputs read: card #21018 (body and all 10 comments: claim 5929373213, os-dev-report 5930446033, ACCEPT held 5931418926, holder note 5944881137, unlock 5944918423, resume 5945191289, ACCEPT lifted 5945609302, landing 5945847631, claim 5945859013, os-dev-report 5946120317); PR #21167's body ("Not in this PR") and thread (records 5930680827 FAIL and 5931398995 PASS, hold 5931049852); PR #21337's body, file list (1 file, +2 / −2, matches) and git diff 222ecc27f9 refs/review/pr-21337; the blobs at the merged main bcd68a29f3 and at the head, by git show; the head's check-runs, polled to convergence. Nothing built, run or re-run.

Checks on 82e06107cf, collapsed latest-per-name, converged at 05:39:01Z: 31 names, 21 success, 10 skipped, 0 failure, 0 pending. Green: Lint & Repo Gates, TypeScript Type Check and its four lanes (source gates, consumer gates, debt ledger, workspace), Test Core and all six shards, Dogfood Regression Gate, Governed Surface Queue Guard, Check Documentation Links, The card this PR closes must claim this branch, Part-of PR must not also close its card, both no-other-open-PR guards, filter. Skipped (path-filtered or opt-in for a one-file skills/** diff): Build Core, Build Docs, Temporal Conformance (live PG + MySQL), Console Pin Gate, Dogfood Verify CLI, the dogfood matrix shard, Packed-tarball smoke (opt-in), Auto Label, Check PR Size, and Check Changeset (short-circuited by the skip-changeset label, see ②).

Shape: the branch is one authored commit 84fc471b7f plus one merge of origin/main whose second parent 222ecc27f9 is the merge-base, so the net diff is the one file; bcd68a29f3 (PR #21167) is an ancestor of both the merge-base and the head. Head repo equals base repo (not a fork). The PR is draft: true, auto_merge: null.

① Derived judgments

(a) The sentence is true at bcd68a29f3. packages/create-objectstack/src/templates/blank/objectstack.config.ts at that commit imports exactly eight barrels, in this order: objects, views, actions, flows, dashboards, apps, skills, picklists (lines 5-12), and hands each to the stack key of its own name through exportsOf (lines 74-81); src/ of the template holds the same eight directories and nothing else. os init's roster is the same list by derivation, not by copy: GENERATORS in generate.ts has eight top-level rows (object, view, action, flow, dashboard, app, skill, picklist; the fields: and list: matches at the same indent are inside the generated-source template strings, not rows; agent and schema are in RETIRED_GENERATORS), GENERATOR_SCAFFOLD_TARGETS = Object.entries(GENERATORS) (generate.ts:614-635), SCAFFOLD_WIRED_BARRELS maps it (init.ts:602-603) and renders the import lines (637) and the stack-key lines (661), and create-objectstack-wiring-parity.test.ts reads that roster and holds the template's imports, helper, stack keys, requires and barrel bytes equal to it. The sentence's order is the template's order, which is the roster's insertion order. The template and roster at the head are byte-identical to bcd68a29f3. The rest of the sentence also holds at that commit: one example object (src/objects/note.object.ts), the three connector executors in plugins: (REST, OpenAPI, MCP), and a scaffolded AGENTS.md (packages/create-objectstack/src/templates/AGENTS.md:49-52, emitted by index.ts:272-296) that explains the barrel-to-exportsOf wiring. Right.

(b) Published rule text: nothing false, nothing newly ambiguous. Each of the eight names is both a src/ directory the generator writes into and a top-level key of ObjectStackDefinitionInput that the same skill already lists (SKILL.md:44-46 names picklists), so "the stack key of its name" is true for the added member exactly as for the seven. picklistExtensions is not a generator barrel and is correctly not listed. The numeral and the list agree, and "generator barrels" still denotes exactly the roster. The sweep confirms no sentence elsewhere in the catalog now disagrees with it. Cost to the customer context window: +13 bytes (eight for seven, same length, plus , picklists in backticks), 5827 to 5830 tokens against the pinned ceiling 5833 (CEILINGS row for skills/objectstack-platform/SKILL.md, script untouched by the diff), recomputed from the blobs under the ratchet's own convention (23306 to 23319 bytes, ceil(utf8 bytes / 4)); the ratchet runs in Type Check · source gates (lint.yml:5851-5854), green. No re-wrap, no ceiling edit. Right.

(c) The sweep, re-done independently over all 65 files under skills/ at the head. Count words (six to nine, 6 to 9) within 60 characters of barrel / generator / template / scaffold / director: one hit, SKILL.md:194, the target. barrel: 16 hits, none a roster (nine generated references/_index.md "root barrel re-exports" lines; bootstrap.md:53-72, a four-barrel example, and :163-180, a one-barrel example; SKILL.md:88 a pointer, :221 a tree comment, :248 a table row; evals/config-plugins-ops.json:7, a CRM eval's expected output; operations.md:200, troubleshooting). picklist: SKILL.md:44-45 (the stack-key list, already correct), :195 (the edit), ui/rules/dashboards.md:352 (unrelated). Tree listings naming barrel directories: only the SKILL.md:219-234 conventions tree. Repo-wide, the strings "seven generator", "eight generator" and "generator barrels" occur only at SKILL.md:194 and packages/cli/src/utils/metadata-file-name.ts:93, a historical measurement comment outside skills/, correctly left (the same reading 5930680827 gave). So nothing else in skills/** states a count or list that #21167 made false. The dev's finding matches. The two notes it raised, judged:

② Semver level

The diff publishes nothing from any released package: skills/ has no package.json of its own; no package.json under packages/** or apps/** at the head names a skills path in files[] (every one scanned; create-objectstack's is dist, README.md, CHANGELOG.md, and its build is the policy sync plus tsup, copying nothing from skills/); the catalog reaches a project only through npx skills add objectstack-ai/objectstack/skills (SKILLS_CATALOG, packages/create-objectstack/src/skills-install.ts), an unpinned catalog spec read from this repository at scaffold time. That is exactly the class AGENTS.md's Post-Task Checklist #3 reserves skip-changeset for ("a diff that publishes nothing from any released package"), so the label is right and no changeset is owed. Check Changeset is skipped by the label (pr-automation.yml:290); the job has no path exemption, so the label is the only green path for this diff. Clause-②: no is right: no accept set moves, no schema, key, flag or argument is added or removed, prose only; no BREAKING, no ADR-0087 marker owed. Right.

③ Boundary flags

  • Deviations, each answered. (1) skip-changeset: right, see ②. (2) One main merge: 82e06107cf has parents 84fc471b7f and 222ecc27f9, 222ecc27f9 is the merge-base, net diff one file +2 / −2, the API file list agrees. (3) The local build under the lock left no trace in the diff. (4) Trailers: 84fc471b7f ends with Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB and Co-authored-by: Claude, the model-free pair AGENTS.md requires; the merge commit carries no trailer (an observation, as on feat(cli,create-objectstack): os generate picklist, the src/picklists starter barrel, and a Picklists count in the metadata summary #21167's merge commit, not a flag). (5) The PR body ends with one session-URL footer and holds zero angle brackets. (6) No model name in the PR body; the model clause in the claim is the seat's dispatch record, not a dev artefact.
  • open_questions: none declared; none found.
  • check:skill-examples NOT MEASURED locally: CI is the measurement. It runs in Type Check · consumer gates (lint.yml:6728), success at 05:29:28Z on this head. The diff changes no ts/tsx fence (both changed lines are prose in a bullet), so the gate's input is unchanged. Measured.
  • The two skills/** readings os-dev.md:290-292 requires: both present in the body and both recomputed here from the blobs: file 489 to 489 lines, 5827 to 5830 tokens; whole catalog (10 SKILL.md) 4397 to 4397 lines, 52132 to 52135 tokens. Correct.
  • ## 维护者速读(草稿): the five fixed sections are present in the required order (改了什么 / 为什么改 / 风险与代价(含回滚) / 席位意见 / 你要做的), 席位意见 is left blank, and 你要做的 names one action (an Approve on this PR), as os-dev.md:287-288 and pm-dispatch SKILL.md:191-192 require. Every claim checks against the diff and the head: two lines changed, net zero; catalog 4397 lines; 5827 to 5830 against ceiling 5833, headroom 3; eight directories with src/picklists the new one; no code, no npm publish; the catalog is pulled by npx skills add from the repository, so projects scaffolded after the merge read the new sentence and existing projects keep their installed copy; rollback is one revert (the squash lands as one commit); the token ceiling is untouched; Tier H needs the maintainer's approval record and the seat lands it. Two framings are slightly wider than the diff, neither false: "本地派生的 25 个门禁全绿" is the dev's local reading, which this record does not re-run, and CI, the measurement, is green; and "不知道…挂在哪个 stack 键下" overstates slightly, since the key picklists was already listed at SKILL.md:44 before this PR; what the stale sentence hid is the directory-to-key wiring of the eighth barrel, which is what the edit restores. Accurate.
  • Fixes #21018 is right. Items 1, 2, 3 and 5 are on main as the single squash bcd68a29f3 (its 14 paths: generate.ts and its pin, format.ts with its pins and the two forced fixtures, cli.mdx, the cli README, the blank template's config and src/picklists/index.ts, the two create-objectstack READMEs, two changesets); PR feat(cli,create-objectstack): os generate picklist, the src/picklists starter barrel, and a Picklists count in the metadata summary #21167 is merged (2026-10-02T04:54:38Z), the ACCEPT 5945609302 and the landing note 5945847631 record it, and the 17.6.0 release that unblocked it is published (5944918423). Item 4 is this diff. The two "Not carried here" bullets are outside the card by its own text. The claim 5945859013 names this branch and Fixes #21018; The card this PR closes must claim this branch and Part-of PR must not also close its card are both green; the only closing keyword in the body is the one Fixes #21018 (feat(cli,create-objectstack): os generate picklist, the src/picklists starter barrel, and a Picklists count in the metadata summary #21167 is cited without one); the card is not in a decision box (labels enhancement, pm:dispatched, domain:cli, priority:p3, area:devpath). Nothing remains on the card after this lands.
  • Tier H. This record is the contract review the claim owes on governed rule text; it is not the landing record. skills/** lifts only on an APPROVED review by a GOVERNED_APPROVERS account; the PR is correctly a draft with no auto-merge, Governed Surface Queue Guard is green, and no seat approves it.

Implemented-by: claude/issue-21018-skill-generator-barrels
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

domain:cli 席 · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-02T05:44Z · 定稿(据 dev 草稿,按本席对 diff 的阅读与契约复核 5946279460 校正)

改了什么

平台技能包 skills/objectstack-platform/SKILL.md 里描述 blank 模板的那一句:「七个生成器目录」改成「八个」,列表末尾补上 picklists。只改 2 行,净零行。

  • 整个技能目录仍是 4397 行。
  • 该文件 token 读数 5827 → 5830,上限 5833 未动。

为什么改

上一个 PR(#21167,已合并为 bcd68a29f3)落地后,npm create objectstack 新建的项目实际接了 8 个生成器目录,多出的是 src/picklists(共享选项列表,os generate picklist 的产物)。技能文本还写着 7 个,AI 读了会少认一个目录,也不知道这个目录要接到同名的 picklists stack 键上。picklists 这个键本身在同一文件第 44 行早已列出,所以这次补的是"目录对应键"这层接线,不是键本身。

风险与代价(含回滚)

席位意见

建议批准。理由:

你要做的

在 PR #21337 上点一次 Approve(你或 os-zhuang 任一账户)。批准记录到位后,由席位转 ready、入队落地,你无需再操作。

@os-zhuang
os-zhuang marked this pull request as ready for review October 2, 2026 05:46
@os-zhuang
os-zhuang enabled auto-merge October 2, 2026 05:46
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 1911d7c Oct 2, 2026
41 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-21018-skill-generator-barrels branch October 2, 2026 06:12
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… Node, re-arming the reroute probe CONTROL leg (objectstack-ai#21356)

Fixes objectstack-ai#21308

Clause-②: no

## What this changes

`tsx` 4.23.15 moved its ESM-API registration out of a `dist/` chunk and
into `dist/esm/api/index.cjs`, two directories deeper. It did not rebase
three file-relative specifiers. On a Node without tsx's
`module.registerHooks` path, the CommonJS build of `tsx/esm/api` then
calls `module.register()` with a hooks URL that names
`dist/esm/api/esm/index.mjs`, a file that does not exist.
`@oclif/core`'s `ts-path` is the caller in this repo. It swallows the
error, logs `Could not find tsx`, and stays on `dist/`.

This PR adds a `pnpm patch` of `tsx@4.23.15` that points the three
specifiers back at the files 4.23.14 resolved. No version moves.

- `patches/tsx@4.23.15.patch`: new, written by `pnpm patch` / `pnpm
patch-commit`.
- `pnpm-workspace.yaml`: the `patchedDependencies` entry and its note,
next to the existing `tsup@8.5.1` entry.
- `pnpm-lock.yaml`: regenerated by `pnpm install`, not edited by hand.
- `packages/cli/test/published-entry-node-env-source-reroute.test.ts`: a
docblock pointer only. No leg, assertion or bound changes.

## Measured

### H0: CI's reading of the CONTROL leg is NOT MEASURED (log hosts
unreachable)

Both log hosts refused this session at CONNECT (`gateway answered 403`):
- `productionresultssa14.blob.core.windows.net`, the job logs of all six
`Test Core` shards of run 36964705223 on 6091136;
- `results-receiver.actions.githubusercontent.com`, the run log zip.

`node scripts/pm/ci-failure.mjs --run 36964705223` exited 2 with `job
log NOT RETRIEVED`.

What was measured instead is the mechanism that decides it. tsx 4.23.12,
4.23.14 and 4.23.15 all take the synchronous `registerHooks` path only
on Node 22.22.3+, 24.11.1+, 25.1.0+ or 26+ (version table
`[[22,22,3],[24,11,1],[25,1,0],[26,0,0]]`). That path never reaches the
broken specifier. Node 22.22.3 was released on 2026-05-13. CI's
`setup-node` asks for `node-version: '22'`, and the dev containers run
v22.22.0. Same tree (6091136), unpatched tsx:

| Node | `published-entry-node-env-source-reroute.test.ts` |
|---|---|
| v22.22.0 | 1 failed (CONTROL), 4 passed |
| v22.22.3 (official tarball, sha256 checked against `SHASUMS256.txt`) |
5 passed |

So CI arms the leg if its runner resolved `'22'` to 22.22.3 or later.
The runner's actual version is in the log this session could not read.

### H1: confirmed, with a version bound

`DEBUG=oclif:config:ts-path` on v22.22.0, CONTROL preload, the test's
own env (no `NODE_PATH`):

```text
Could not find tsx. Skipping tsx registration for .../packages/cli.
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../tsx/dist/esm/api/esm/index.mjs' imported from .../tsx/dist/esm/api/index.cjs
```

Next it logs `Cannot find module 'ts-node'`, and `tsPath` returns the
`dist/` path. In the tarballs, 4.23.12, 4.23.13 and 4.23.14 keep the
`register()` call in `dist/register-*.cjs`, where it resolves. 4.23.15
inlines it into `dist/esm/api/index.cjs`, which grows from 1,298 to
16,770 bytes. The `.mjs` build of `tsx/esm/api` is unaffected. oclif
reaches the `.cjs` build because it locates the module with
`require.resolve`.

### H2: holds

`npm view tsx dist-tags` gives `latest` as 4.23.15 (2026-09-20). There
is no newer release, so no UP move (remedy d) exists.

### A second casualty of the same defect

On v22.22.0 with unpatched tsx, `bin/run-dev.js` also lands on
`dist/commands`. That file is the CLI's SOURCE entry, the one `runServe`
and the other spawning tests launch (104 test files under `packages/cli`
mention it). `tsx bin/run-dev.js --version` under
`DEBUG=oclif:config:ts-path` logs `Could not find tsx` and never `Found
source directory`. With the patch it logs `Found source directory for
.../dist/commands at .../src/commands`. So on such a Node, source-entry
tests have measured the build output rather than `src/` since objectstack-ai#21162.
The same patch fixes this with nothing added.

## Why (b), and not (a) or (c)

**(a)** pins tsx down to 4.23.14. That is a DOWN move and needs a
ruling, and measurement does not make it the only sound remedy. Not
taken.

**(c)** reworks the CONTROL leg. To arm without oclif's own tsx
registration, the child would need a test-only change to how it resolves
`tsx/esm/api`: a `Module._resolveFilename` shim in the preload, or
`--conditions=import` on the child. That fails the second acceptance
criterion as measured. The absence legs do not load the preload, so on
v22.22.0 with unpatched tsx, deleting the declaration leaves them green
(row 4 below). Arming them too would put the shim in every leg, and then
every leg measures a resolution no real install has.

**(b)** patches the component that fails. The test's behaviour is
unchanged, and both acceptance criteria hold on v22.22.0 and on
v22.22.3.

Four axes:
- **Real business need:** measured. Two consumers break on Node below
22.22.3. One is this probe's CONTROL leg. The other is the source entry
`bin/run-dev.js`, which is the larger one, because the CLI's spawning
tests silently ran `dist/`. The published CLI is not reached:
`bin/run.js` sets `settings.enableAutoTranspile = false`, so oclif never
registers tsx there, and nothing in `packages/**` source imports
`tsx/esm/api`.
- **Long-term soundness:** the patch is a workaround for an upstream
packaging defect, and it has a cost. It replaces one 16 KB minified
line, which nobody can review by eye. The token comparison below is how
to review it. The key names the exact version, so a tsx bump fails `pnpm
install` (`ERR_PNPM_UNUSED_PATCH`, the same rule the `tsup@8.5.1` entry
records), and the patch cannot outlive its reason silently. Retire it
when a tsx release resolves the three paths itself, or when the
toolchain's Node floor reaches 22.22.3. (c) would carry a test shim with
no such tripwire. After an upstream fix it would become dead code that
still makes every leg diverge from a real install.
- **Making AI mistakes harder:** (b) gives the probe one meaning in
every environment. Today the same file is green in CI and red in the dev
containers, and three devs reported that red in passing, without a card,
because it read as "environment". (c) adds a second resolution dialect
inside a test fixture, which is the consumer-side tolerance that
contract-first rules out.
- **Startup focus:** no new gate, no new dependency, no version
movement. Three tracked files, plus the lockfile the tooling wrote.

## CONTROL leg and reverse verification (Node v22.22.0 unless stated)

| tree | tsx | declaration in `bin/run.js` | `development` / `test` legs
| CONTROL (development, neutralised) |
|---|---|---|---|---|
| 6091136 (main) | unpatched | present | pass | FAIL: output is only
`@objectstack/cli/17.6.0 linux-x64 node-v22.22.0` |
| cc0c05b | patched | present | pass | pass: card signature present,
exit non-zero |
| cc0c05b | patched | deleted | FAIL x2: `expected '(node:...)
[MODULE_NOT_FOUND] Warni...' not to contain 'Cannot find module
'./registry''` | pass |
| cc0c05b, `packages/cli/node_modules/tsx` linked to the pristine
4.23.15 | unpatched | deleted | pass (vacuous: 1 failed, 4 passed) |
FAIL |
| 8569d5f (final) | patched | present | pass | pass (5 passed) |
| 8569d5f, Node v22.22.3 | patched | present | pass | pass (5 passed)
|

`scripts/ablation-replace.mjs --delete` removed the declaration (anchor
1 to 0, blob `569e6b6f` to `40a368ce`). The same tool restored it and
showed the blob equal to HEAD's, with `git diff HEAD` empty. The tsx
link swap ran under an EXIT/INT/TERM trap, and `readlink` confirmed the
restore. `bin/run.js` runs unbuilt, so no `dist/` step is involved.

## Reviewing the patch

The patch replaces one minified line. Review it with this comparison,
which splits the pristine and the patched file on commas and prints
every token that differs:

```sh
npm pack tsx@4.23.15 && tar xzf tsx-4.23.15.tgz
node -e 'const fs=require("fs");const a=fs.readFileSync(process.argv[1],"utf8").split(","),b=fs.readFileSync(process.argv[2],"utf8").split(",");let n=0;for(let i=0;i!==a.length;i++)if(a[i]!==b[i]){n++;console.log("-",a[i].slice(-60));console.log("+",b[i].slice(-60))}console.log(n,"of",a.length,"differ")' package/dist/esm/api/index.cjs node_modules/.pnpm/tsx@4.23.15_patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6/node_modules/tsx/dist/esm/api/index.cjs
```

Output:

```text
- se=[new URL("loader.mjs"
+ se=[new URL("../../loader.mjs"
- new URL("esm/index.mjs"
+ new URL("../index.mjs"
- essageChannel;R.register(`./esm/index.mjs?${_.randomUUID()}`
+ e.MessageChannel;R.register(`../index.mjs?${_.randomUUID()}`
3 of 514 comma-separated tokens differ
```

The `register()` specifier is the one that breaks things. The two `new
URL(...)` entries feed tsx's `isTsxImport` set and carry the same wrong
base. They are fixed in the same patch because they are the same defect,
in the same file, with the same mechanical change. Both rebased targets
exist on disk (`dist/esm/index.mjs`, `dist/loader.mjs`). No test here
reaches those two entries, because no child in this suite passes a
TypeScript preload ahead of an absolute tsx loader path.

## Lockfile

`pnpm install` regenerated it. With the tsx patch hash normalised away,
every removed line equals an added line, except for the three new
`patchedDependencies` lines. 0 versions moved, 0 DOWN, 0 packages added
or removed. Ten snapshot keys change only by the hash suffix: `tsx`
itself, and the peer suffix of `tsup`, `postcss-load-config`,
`fumadocs-mdx`, `vite` x2, `vitest` x2 and `@vitest/mocker` x2. `pnpm
install --frozen-lockfile` passes on the final head.

## Verification

- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` at 8569d5f derives 58 commands from 4 paths. All 58 were
run with exit codes captured before any pipe, and all exited 0. `--ran`
reports 58 derived, 58 run, 0 NOT-MEASURED. (In an earlier pass at
6a22902, `check:dual-build-cjs-loads` answered PREREQUISITE NOT MET
while sibling packages had no `dist/`. Once those were built it passed:
105 entry points across 66 packages.)
- `@objectstack/cli` unit tier at 6a22902: 244 files, 3461 tests
passed.
- `@objectstack/cli` integration tier at 6a22902, two runs under the
verify lock: 71 files, 607 tests passed, 1 skipped. The skip is the
existing `it.skipIf` in `test/migrate-meta-default-range.test.ts`, a
file this branch does not touch.
- `pnpm --filter @objectstack/cli typecheck` at 6a22902: exit 0,
`check:test-typecheck` included.
- 6a22902 to 8569d5f is two merges of `main`: objectstack-ai#21317
(plugin-security), objectstack-ai#21335 (objectql, rest, plugin-auth), and two
docs-only commits (objectstack-ai#21337, objectstack-ai#21343). None touches a CLI file, the
lockfile, `pnpm-workspace.yaml` or tsx, so the CLI tiers were not re-run
for them. The packages the merge touched (objectql, plugin-auth, rest)
were rebuilt. Then the reroute file (5 passed on v22.22.0, 5 passed on
v22.22.3) and all 58 gates were re-run on 8569d5f.
- Not run here: `pnpm lint` (repo-wide, CI's run).

**skip-changeset:** nothing published moves. `@objectstack/cli` ships
only `dist`, `README.md` and `CHANGELOG.md`, so the test file is not
published. `patches/`, `pnpm-workspace.yaml` and `pnpm-lock.yaml` are
root files no package ships. The CLI's dependency range on tsx is
unchanged.

## Acceptance notes

- The dev containers run Node v22.22.0, while CI's `setup-node` floats
on `'22'`. That gap is why the same file read red in three dev
containers and green in `Test Core`. Observation only, no card. Carrier:
none.
- Upstream: whether tsx already has an issue or fix in flight was not
read, because this session cannot reach `privatenumber/tsx` through the
API. The retirement condition is in the `pnpm-workspace.yaml` note.
- Published reach: `@objectstack/cli` depends on `tsx ^4.23.15` at
runtime, so a customer on Node 22.0 to 22.22.2 installs the defective
build. No published code path calls `require('tsx/esm/api')`, so no
public entry point reaches it. Measured by a grep of `packages/**`
source and by the published entry's `enableAutoTranspile = false`.
- `packages/cli/README.md` and `packages/cli/package.json` belong to
objectstack-ai#21310 and are not touched.

---
_Generated by [Claude
Code](https://claude.ai/code/session_018gA1pE6eJtwHhqx72G8U9X)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 needs-user-decision size/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants