Skip to content

feat(cli,create-objectstack): os generate picklist, the src/picklists starter barrel, and a Picklists count in the metadata summary - #21167

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21018-generate-picklist
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-21018-generate-picklist

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Part of #21018
Clause-②: yes (widening)

This PR is the code half of #21018. It covers scope items 1, 2, 3 and 5 of the card body. Item 4, the "seven generator barrels" line in skills/objectstack-platform/SKILL.md, is on a Tier H governed path. It follows in its own PR under a second claim, so #21018 remains open for it.

Dispatched by the domain:cli seat's PM under claim 5929373213. Dev session session_01VvcEokUG1tvVxkceYfR5XB.

Premise, checked on origin/main

The card waited on the runtime reader. That reader is on main as 88b484e (#21047), and these are the parts this PR relies on:

  • picklists and picklistExtensions are in METADATA_ARRAY_KEYS (packages/objectql/src/engine.ts).
  • The registry fold resolves a field's picklist into served options (picklist-resolution.ts, resolvePicklistFieldsOnto).
  • The write door judges a write against the resolved options.
  • The boot refuses an unresolved name (packages/objectql/src/plugin.ts).

The repro below measures all of this end to end on a scaffolded project.

What changed

Item 1: os generate picklist NAME (packages/cli/src/commands/generate.ts)

Item 2: scaffold wiring

  • os init derives its wiring from the roster (SCAFFOLD_WIRED_BARRELS), so the app and plugin templates wire src/picklists with no edit to init.ts.
  • The blank starter in create-objectstack gains src/picklists/index.ts, byte-identical to the empty barrel os init writes. Its objectstack.config.ts gains import * as picklists from './src/picklists'; and picklists: exportsOf(picklists),.
  • create-objectstack-wiring-parity.test.ts holds the two scaffolders equal.

Item 3: docs

  • content/docs/deployment/cli.mdx gets the example line, the table row (picklist, src/picklists/, NAME.picklist.ts, picklists) and the "What it does" clause.
  • packages/cli/README.md gets the type list and a short paragraph.

Item 5: the metadata summary (packages/cli/src/utils/format.ts)

  • MetadataStats gains picklists, and collectMetadataStats counts it through the existing authoringRuleUnionStack fold, so both ADR-0130 D4 shapes are covered.
  • printMetadataStats renders it in the Data: row, the kind's own domain (domain: 'data' in the registry).
  • A stack with no picklists prints the row it printed before. A picklistExtensions entry is not counted as a list.
  • The two existing test fixtures that spell out every MetadataStats member gain picklists: 0: format.metadata-stats-package-fold.test.ts and print-metadata-stats-zero-row.test.ts.

Changesets

  • @objectstack/cli: minor. A new generator kind, and stats.picklists is added to the --json output of os validate, os build and os info.
  • create-objectstack: minor. The starter gains a wired barrel.
  • Both carry Clause-②: yes (widening). Nothing is narrowed and nothing is renamed, so there is no ADR-0087 marker.

Repro, before and after

Before, on origin/main 58a77db:

os generate picklist industry      exit 1   ✗ Unknown type: picklist   (roster lists 7 types)
npm create objectstack (blank)     src/ = actions apps dashboards flows objects skills views
hand-wired picklist + select field
  os validate                      exit 0   Data: 1 Objects  3 Fields

After, on this branch:

npm create objectstack (blank)     src/ = actions apps dashboards flows objects picklists skills views
os generate picklist industry      exit 0   ✓ Reaches the stack: objectstack.config.ts carries it in `picklists` as 'industry'
note.object.ts gains  industry: Field.select({ picklist: 'industry', label: 'Industry' })
  os validate                      exit 0   Data: 1 Objects  3 Fields  1 Picklists
  os build                         exit 0   Data: 1 Objects  3 Fields  1 Picklists   (artifact carries `picklists` and the field's `picklist`)
  os info                          exit 0   Data: 1 Objects  3 Fields  1 Picklists
  os info --json                   stats.picklists = 1
os dev --fresh -p RANDOM
  GET  /api/v1/meta/object/my_app_note   200   fields.industry = { picklist: 'industry', options: [option_a, option_b], … }
  GET  /api/v1/meta/picklist/industry    200
  POST /api/v1/data/my_app_note { industry: 'option_a' }   201
  POST /api/v1/data/my_app_note { industry: 'option_z' }   400 VALIDATION_FAILED · invalid_option · names picklist "industry"

On a project scaffolded before this change (its config wires no src/picklists), os g picklist region exits 0. It reports Not wired and prints the import line and the defineStack key to add.

Tests

The pins are:

  • packages/cli/src/commands/generate-picklist.pin.test.ts:

    • the roster row (src/picklists, stack key picklists, namesObject: false, no requires) and the NAME.picklist.ts file name;
    • the scaffold loaded through the loader os validate uses (bundle-require, BUNDLE_REQUIRE_EXTERNALS), parsed by PicklistSchema, under the item name os g reports;
    • that scaffold registered through ObjectQL.registerApp under picklists, serving its options on a Field.select({ picklist }) field, and passing PicklistServedFieldSchema;
    • a control: the same field with no list serves no options.

    It constructs ObjectQL, so it lands in the integration tier.

  • packages/cli/src/utils/format.metadata-stats-picklists.test.ts:

    • the count, top level and option-B;
    • that an extension is not a list;
    • the zero case;
    • the rendered row Data: 1 Objects 2 Fields 1 Picklists;
    • a control: a stack with no picklists prints Data: 1 Objects 2 Fields.

The runs, all on HEAD 5a53515 except where noted:

Run Files Tests Result
pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 241 + 2 3427 + 29 All passed, 29 skipped. The 2 files first failed with packages/cli is not built and passed after pnpm --filter @objectstack/cli build.
--project integration, the 8 integration files among the related tests 8 66 Passed: generate-picklist.pin, create-objectstack-stack-reach, generate-stack-reach, info-detail-package-fold and 4 more
OS_TEST_TIERS=nightly, the related *.e2e files 6 42 Passed. Includes generate-scaffolds-reach-stack.e2e: "os g picklist exits 0 and reports no wiring to add"
pnpm --filter create-objectstack exec vitest run 16 249 Passed
pnpm --filter @objectstack/cli typecheck (tsc --noEmit && check:test-typecheck) Exit 0. The test-typecheck ledger is unchanged (3 files / 28 errors)
pnpm --filter create-objectstack typecheck Exit 0

Ablation

The ablations ran on the committed fix (e63b1db). Each mutation went through scripts/ablation-replace.mjs with the anchor counted on disk. Each restore was proven: the blob equals HEAD and git diff HEAD is empty.

Leg Mutation Red Control (green)
generator The picklist row deleted from GENERATORS (anchor x1 → x0, 53 lines) generate-picklist.pin 3 of 5. wiring-parity: imports and stack keys (2) generate-scaffold-validates 19/19; the pin's file-name case and its no-list control
template picklists: exportsOf(picklists), deleted from the blank config wiring-parity: "hands every wired barrel to its stack key" (1 of 22) the other 21
collect picklists: count(stack.picklists), deleted metadata-stats-picklists 5 and package-fold 4 (its ZEROES comparisons) the pin's no-picklists row; print-metadata-stats-zero-row
print ['Picklists', stats.picklists], deleted metadata-stats-picklists "Data: row", and print-metadata-stats-zero-row "every metric collectMetadataStats counts is rendered" the other 27

Gates

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run with no paths, derived 96 families. All 96 were run, each with its exit code recorded before any pipe.
  • Five of them first refused with PREREQUISITE NOT MET (exit 3) because their build inputs were missing: check:skill-examples, check:dual-build-cjs-loads, check:i18n, check:i18n-coverage and check:i18n-walk-parity. All five passed once those inputs were built.
  • --ran reconciliation: ✓ dispatch-gates --ran: 96 derived famil(ies) accounted for — 96 run, 0 NOT-MEASURED.

pnpm lint, as a proven narrowing (on HEAD 5a53515):

  1. Population, read from eslint's own config (isPathIgnored / calculateConfigForFile): all 8 touched .ts files are linted. The 4 touched .md / .mdx files are outside its population.
  2. Count, from --format json: 8 files, 0 errors, 0 warnings, with --no-inline-config.
  3. Invariance: no resolved config for these files is type-aware (no parserOptions.project, no projectService, as eslint.config.mjs states). This diff touches no lint config or baseline. So it cannot move the verdict on any file it does not touch.

Not in this PR

Acceptance notes

  • The two starter README listings now name src/picklists (commit 90d55b1e13, added on contract review 5930680827):

    • the Layout bullet in packages/create-objectstack/src/templates/blank/README.md, which ships into every new project;
    • the tree in packages/create-objectstack/README.md.

    They ride this PR, not the Tier H item-4 PR, so two non-governed lines do not wait on a human approval.

  • Scaffold with repo dist is red on this PR, and the PR is held until the next release publishes. This note was added by the seat.

    • Cause: the job scaffolds with the repo-built create-objectstack, then installs the project's framework from the npm registry. The template's ^17.0.0 range resolves to the published @objectstack/spec 17.5.0, which predates the picklists stack key (addbbf0, .changeset/19518-picklist-kind.md, still unreleased). So the generated project's defineStack refuses the key as unrecognized at the validate step.
    • Scope of the measurement: the repro table's npm create objectstack rows and the end-to-end chain were measured on the repo build, not against the registry.
    • It is not this PR's code to fix. Its gate checks the template against the real registry, and the template now runs one release ahead of it.
      • The check is not required. It runs only on pull_request under its path filter (packages/create-objectstack/**, docker/** and its own workflow file), and it is not red on main.
      • Landing now would leave it red on every later create-objectstack PR until that release.
    • The hold: the release that carries the key is the open Version Packages PR chore: version packages #20639; its @objectstack/spec changelog already lists addbbf0. Once it merges and publishes, a re-run of this job reads the new spec and the template is coherent with it.
    • Until then, the PR stays draft and its card is pm:blocked on chore: version packages #20639.
  • On a project scaffolded by an earlier release, the not-wired hint prints picklists: Object.values(picklists),, not the starter's exportsOf(...) spelling. Both type-check once the barrel exports a list, which is the only state the hint is printed in.


Generated by Claude Code

claude added 4 commits October 1, 2026 10:34
… starter barrel, and a Picklists count in the metadata summary

os generate picklist NAME writes NAME.picklist.ts through definePicklist into
src/picklists, collected under the picklists stack key. os init derives its
wiring from the roster; the blank starter of create-objectstack gains the same
empty barrel and config lines. collectMetadataStats counts picklists, printed
in the Data: row of os validate, os build and os info.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…, with changesets

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…project sees, as measured

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

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, create-objectstack, objectstack-blank, touching 7 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/cli/README.md, packages/create-objectstack/README.md, packages/create-objectstack/src/templates/blank/README.md, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/deployment/cli.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/getting-started/your-first-project.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/objectql/types.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))

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

  • content/docs/releases/v17/17-0.mdx (via MetadataStats (symbol, a top-level interface))
  • content/docs/releases/v17/17-4.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/releases/v17/17-5.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))

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

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/cli/README.md, packages/create-objectstack/README.md, packages/create-objectstack/src/templates/blank/README.md, …) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 32 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1371dc980cdf0d3128bee4a5441f2c6bec18f008 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4594faf5e1e5e7a3659b2e410762ce57a1e7c9b6 — the merge of head cd1910a947360721404bab9d960b4e955914d453 into base 1371dc980cdf0d3128bee4a5441f2c6bec18f008, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4594faf5e1e5e7a3659b2e410762ce57a1e7c9b6 && git checkout 4594faf5e1e5e7a3659b2e410762ce57a1e7c9b6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1371dc980cdf0d3128bee4a5441f2c6bec18f008 cd1910a947360721404bab9d960b4e955914d453 && git checkout -B drift-repro 1371dc980cdf0d3128bee4a5441f2c6bec18f008 && git merge --no-ff cd1910a947360721404bab9d960b4e955914d453

node scripts/docs-audit/affected-docs.mjs --json 1371dc980cdf0d3128bee4a5441f2c6bec18f008

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 1371dc980cdf0d3128bee4a5441f2c6bec18f008 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5a535158c74d2b9e87e5adf7f03a5d87f477c54a
Local-runs: none

Inputs read: card #21018 (body, claim 5929373213, os-dev-report 5930446033); #20825 (answer B, dev report 5922744843, addendum 5922965902); #19519 and PR #21047 (88b484e00c, an ancestor of the head); the reverted row 6ef78d3f29 → e16359a9fa; PR #21167 body, file list (12 files, +329 / −5, matches) and git diff 70dae533c5 refs/review/pr-21167; the head's check-runs, polled to convergence; the head's sources by git show.

Checks on the head, collapsed latest-per-name, converged at 11:44Z: 37 names, 33 success, 3 skipped (Console Pin Gate, Packed-tarball smoke opt-in, Registry canary), 1 failure: Scaffold with repo dist (.github/workflows/scaffold-e2e.yml, job 110347683090, step 11 "Validate and build the generated project"). The required seven (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) are all success. The red is NOT red on origin/main: that job runs only on pull_request under a packages/create-objectstack/** path filter (the nightly schedule runs the published-registry canary only), origin/main bafb8c9498 carries no check-run of that name, and the previous run of the same job, PR #21162 at a2d5d53c4e (11:06Z), was green. The red is this diff's; see ① (b).

Mergeability: git merge-tree --write-tree of the head onto a freshly fetched origin/main (bafb8c9498, fetched into refs/review/main-21167) exits 0 with no conflicted path. None of the 12 paths is routed to the merge=os-regen driver, so the local answer is GitHub's answer. Merge-base is 70dae533c5 (one main merge, as declared); main's 7 commits since touch none of the 12 paths.

① Derived judgments

(a) Item 1, the generator. The emitted file is definePicklist({ name: snake(NAME), label: Title(NAME), options: [two label/value entries] }), which is valid against PicklistSchema at the head (packages/spec/src/data/picklist.zod.ts: strict object, name SnakeCaseIdentifier, label string, options min 1 SelectOptionSchema; definePicklist parses at module load). The pin loads the scaffold through bundleRequire with BUNDLE_REQUIRE_EXTERNALS and parses it with PicklistSchema; green in Test Core. Conventions hold: the filename comes from metadataFileName('picklist', …), i.e. the registry row (kernel/metadata-plugin.zod.ts:825, filePatterns[0] **/*.picklist.ts, domain: 'data', loadOrder: 8), no override; the charset gate (nameCharsetRefusal, asking ObjectSchema.shape.name) runs for every type before anything is derived; namesObject: false is right (a picklist name is judged by no namespace gate, so neither the prefix nor the load-failed refusal applies); requires: [] is right (no capability token); the retired ledger is untouched; the reach report (measureStackReach / wiringLines) is per-generator generic, so "Reaches the stack" / "Not wired" come for free, and the e2e generate-scaffolds-reach-stack iterates the roster. The stack key is singularToPlural('picklist') = picklists through the SINGULAR_TO_PLURAL row (meta-spelling/manifest-collection-spelling.ts:93), the same key METADATA_ARRAY_KEYS reads first (packages/objectql/src/engine.ts:2962) and registerApp registers; the pin proves it end to end (served options equal the list's, PicklistServedFieldSchema passes, control with no list serves none). Enumerations of generator kinds: the oclif help text is Object.keys(GENERATORS) (generate.ts:3518), the Unknown type: listing iterates GENERATORS, GENERATOR_SCAFFOLD_TARGETS → SCAFFOLD_WIRED_BARRELS are derived, and the 14 roster-reading pins extend by themselves; generate-emission-parses.test.ts asserts containment and length ≥ 7 (its title still says "seven", harmless); the two prose counts in generate.ts are reworded; the cli README types list and the cli.mdx example, table row and "What it does" clause moved. metadata-file-name.ts:93 "across the seven generators" is a historical measurement and is correctly left. The roster counts still false at the head are the Tier H skills/objectstack-platform/SKILL.md:194-195 (item 4, owed) and the two create-objectstack README listings (③).

(b) Item 2, the starter. Structural half holds. The blank config gains the import and picklists: exportsOf(picklists),; src/picklists/index.ts is byte-identical to renderEmptyWiredBarrel('picklists', 'picklist') (init.ts:671-677). os init derives both the app and plugin templates from SCAFFOLD_WIRED_BARRELS = GENERATOR_SCAFFOLD_TARGETS.map(…) (init.ts:602-603; renderWiredImports / renderWiredStackKeys / withWiredBarrels at 752/778/781 and 846/873/876): one list, no second. create-objectstack-wiring-parity.test.ts reads that roster and TEMPLATES.app's render and compares the import lines, the helper, the stack-key lines, the requires tokens and each empty barrel's bytes against the template, so it holds the two together in both directions (the dev's row-absent and key-absent legs red 2 and 1 respectively, as its structure predicts).

Behavioural half: against the REPO build a fresh project validates and builds — create-objectstack-stack-reach.test.ts (integration: real bin/ scaffold → os g → os validate on workspace copies) and the dev's chain. Against the PUBLISHED framework it does NOT, and the head's own check says so: Scaffold with repo dist scaffolds with the repo-built scaffolder, npm installs the template's ^17.0.0 pins from the registry (resolves to 17.5.0, published 2026-09-29T08:09Z, no fallback taken, 446 packages), and npm run validate exits 1 with ✗ (root): Unrecognized key(s) on this stack definition: picklists from the published defineStack. The key entered ObjectStackDefinitionSchema at addbbf02ab (2026-09-30, #20823) and its changeset .changeset/19518-picklist-kind.md is still pending at the head, so no published spec accepts it. That job's contract, recorded at d51106d4dc (#9374), is the template against the real registry; the template now runs ahead of it. Consequence: the check stays red on every PR touching packages/create-objectstack/** until the lockstep release that carries that changeset publishes @objectstack/spec with the key; after that release the first-run path is coherent by construction (one changeset publish of the 69-package fixed group; the only residual is the publish-order window inside a release, 17.5.0 published create-objectstack 14 minutes before spec, which is a release-process property, not this PR's). Nothing inside this claim's surface can green it earlier: holding the wiring is what the card's own measurement ("4 red with the row alone") rules out, and the e2e workflow is outside the surface. What the PR owes and does not carry is the disclosure: the body says nothing about the red; its "After" table reads npm create objectstack (blank) … os validate exit 0 … 1 Picklists without saying it was measured on the repo build, and the os-dev-report reads CI as "22 in_progress (not waited on)" although the job had completed red at 11:31:15Z, 92 seconds before the report. An advisory red that lands undisclosed rides every later create-objectstack PR until stanched. This is the FAIL.

(c) Item 3, docs. content/docs/deployment/cli.mdx: the example line, the table row (picklist / src/picklists/ / NAME.picklist.ts / picklists / description) and the "What it does" item 2 clause match the roster, the registry and #21047's served behaviour; item 1's object-naming list correctly omits picklist. packages/cli/README.md: the types list and the three-line paragraph are accurate. No content/docs/releases/** and no CHANGELOG.md in the diff. Check Documentation Links green.

(d) Item 5, the stats row. collectMetadataStats counts count(stack.picklists) over authoringRuleUnionStack(config): the number of picklist DECLARATIONS in the union stack (top level, or each packages[] body, the same fold every other member uses), never references, and picklistExtensions is a different key so an extension is not a list (pinned). printMetadataStats adds ['Picklists', stats.picklists] to Data: between Fields and Extensions; the loop keeps items.filter(v > 0) and zeroFallback: ['Objects'] is unchanged, so a stack with no lists prints the row it printed before (pinned: Data: 1 Objects 2 Fields), and the zero-row pin's "every metric collectMetadataStats counts is rendered" plus the MetadataStats interface enforce from both ends. --json: stats is spread whole into three emitJson object literals (validate.ts:859, compile.ts:1144, info.ts:75); there is no declared Zod or TS payload type; build-json-advisory-parity.e2e.test.ts:306-322 pins the TOP-LEVEL key set only, with stats as one key, so stats.picklists is additive inside an open shape and the three payloads stay mutually consistent through the one interface.

(e) Pins and ablations. generate-picklist.pin.test.ts (5 cases) and format.metadata-stats-picklists.test.ts (6 cases) read as described. The four legs are consistent with the pins' structure: row deleted → PICKLIST undefined reds the three PICKLIST! cases, the filename case and the no-list control stay green (3/5), and parity's imports and stack-keys cases red (2); key deleted → parity's stack-keys case alone (1/22); collect line deleted → 5 of 6 in the new file (the control stays green because undefined > 0 is false) plus the 4 ZEROES comparisons; print row deleted → the Data-row case and the every-metric pin. The two fixture edits (picklists: 0) are forced by the interface (toEqual and typecheck). The reported chain is consistent with the code: served options and the 400 invalid_option naming the list are #21047's registry fold and write door; GET /meta/picklist/industry 200 is #21047's artifact-door mapping; os validate refusing an unknown reference is #21003's utils/picklist-references.ts, present at the head. One caveat on (e) as reported: the chain was run on the repo build only; see (b).

② Semver level

@objectstack/cli minor: a new accepted generator kind and a new stats.picklists key on three published --json payloads are "a new accepted key or value" under WHICH LEVEL (pr-automation.yml:754-793), so at least minor; feat( does not lower it. create-objectstack minor: the published package's output gains a file and a wired key, purely additive, under a feat(; minor is never below what the act requires, and the fixed group of 69 (both packages in it) versions in lockstep regardless. Both changesets carry Clause-②: yes (widening), so check-changeset-no-major.mjs's level axis (declared yes → at least one moved package ≥ minor) is satisfied; no major; nothing removed or renamed, so no BREAKING banner and no ADR-0087 marker is owed. Check Changeset green. Right.

③ Boundary flags

  • Deviations, each confirmed: the turbo-written AGENTS.md block is not in the diff (no governed path among the 12; fde553c509 sits on main after the merge-base, as stated); the two fixture edits are inside item 5's direct fixtures and forced by the interface; the item-2 measurement shape (row-absent 2 red, key-absent 1 red) is sound and matches the parity pin's structure; exactly one main merge, 70dae533c5 is the merge-base, not re-merged, main's 7 later commits are disjoint, merge-tree clean; the lint narrowing is superseded by Lint & Repo Gates green on this head; the three authored commits end with the model-free pair (Claude-Session, Co-authored-by: Claude), the merge commit 5a535158c7 carries no trailer at all (an observation, not a flag); Clause ② re-read matches the diff.
  • Out-of-scope finding, real at the head: packages/create-objectstack/src/templates/blank/README.md:70-76 ("Layout") and packages/create-objectstack/README.md:79-88 ("What Gets Generated") both omit src/picklists. The blank README ships into every new project and sits INSIDE the directory the claim names for item 2, so it belongs in this PR; the package README is one line in the same non-governed package and may ride with it. Neither belongs on the Tier H item-4 PR, which would make two non-governed lines wait on the maintainer's word.
  • Part of #21018 is the first line; no closing keyword anywhere in the body; Part-of PR must not also close its card green. Right, since item 4 remains.
  • No governed path in the diff: nothing under skills/**, docs/adr/**, .claude/**, docs/NORTH-STAR.md, AGENTS.md, CLAUDE.md; Governed Surface Queue Guard green. The ADR-0063 anchor on generate.ts is unaffected (the agent row stays in RETIRED_GENERATORS; check:adr-anchors green inside Lint & Repo Gates).
  • Item-4 line handed over: "the eight generator barrels (objects, views, actions, flows, dashboards, apps, skills, picklists)" is accurate to the roster at the head (8 rows, keys by singularToPlural), and SKILL.md:194-195 at the head still reads the db48028f1a text the dev quotes.

Remedy for the FAIL (no code change needed; a body edit leaves this head-sha standing): (1) add an Acceptance note naming the Scaffold with repo dist red, its cause (the template wires picklists; the scaffolded project installs the published spec 17.5.0, which predates the key; .changeset/19518-picklist-kind.md pending), its reach (every packages/create-objectstack/** PR until the release that carries that changeset) and that the repro chain was measured on the repo build; (2) the owning seat records on the PR whether it lands now with the advisory red or holds until that release — a needs-user-decision if the seat cannot; (3) carry the blank README "Layout" line here (and the package README line with it), not on the Tier H PR.

Implemented-by: claude/issue-21018-generate-picklist
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: FAIL

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Scaffold with repo dist is red on 90d55b1e13, as it was on 5a535158c7, and this PR is held. It is not readied or queued.

domain:cli seat · session_01VvcEokUG1tvVxkceYfR5XB · 2026-10-01T12:07Z · follows the contract review 5930680827

  • What fails: the job scaffolds with this branch's create-objectstack, installs the framework from npm, and npm run validate refuses the template's picklists stack key as unrecognized. The registry's latest @objectstack/spec (17.5.0) predates that key.
  • Why it is not fixable here: the key's changeset (addbbf0, .changeset/19518-picklist-kind.md) is unreleased. The check is not required, but landing with it red would leave it red on every later create-objectstack PR. It is not red-by-design in its workflow's own terms, so the queue rule does not let this PR carry it.
  • What unblocks it: the release in the Version Packages PR chore: version packages #20639, whose @objectstack/spec changelog already lists addbbf0. After it publishes, the seat re-runs this job once and lands the PR if it is green.
  • No re-run now: the failure is deterministic on the published spec, so a re-run would fail identically.
  • The PR body's Acceptance notes carry the same reading.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 90d55b1e13821e4f8018776b809ebc8917b427aa
Local-runs: none

Re-review of PR #21167 after the FAIL record 5930680827 on 5a535158c7. Read: git diff 5a535158c7 refs/review/pr-21167 (re-fetched from refs/pull/21167/head), the PR body's Acceptance notes from the API, the hold comment 5931049852, the head's check-runs (converged), .changeset/19518-picklist-kind.md at the head, and #20639's head 718e74c744 through git show (fetched into refs/review/pr-20639). Unchanged detail is not restated; 5930680827 carries it.

What moved. One commit, 90d55b1e13 ("docs(create-objectstack): both starter README listings name the src/picklists barrel", model-free trailer pair present), touching exactly packages/create-objectstack/src/templates/blank/README.md (the Layout bullet gains src/picklists/, in the "empty to start" list, same sentence otherwise) and packages/create-objectstack/README.md (the "What Gets Generated" tree gains picklists/index.ts as the last leaf, skills re-drawn as a branch). Both lines are accurate to the roster at the head (eight generators, insertion order ending in picklist) and to the starter (an export {}; barrel wired under picklists through exportsOf). Every other blob of the 14-file diff against the merge-base 70dae533c5 is byte-identical to 5a535158c7 (checked per path by blob id: the ten code, test, doc and changeset files and the two template files). Total now 14 files, +336 / −11, matching the API.

Checks on 90d55b1e13, collapsed latest-per-name, converged (0 pending): 37 names, 31 success, 5 skipped (Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke opt-in, Registry canary), 1 failure: Scaffold with repo dist (job 110359370992), red at the same step 11 "Validate and build the generated project" with the same message, ✗ (root): Unrecognized key(s) on this stack definition: picklists, after npm install resolved the template's ^17.0.0 to the registry's 17.5.0. The required seven (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard) are all success. Still not red on origin/main: e35c40a525 carries no check-run of that name and no failure at all, the job runs only on pull_request under its path filter, and the next run of the same job after this one, PR #21162 at 0f87e84889 (12:12Z), was green. The red remains this diff's, deterministic on the published spec.

Mergeability. git merge-tree --write-tree onto a freshly fetched origin/main (e35c40a525, into refs/review/main-21167) exits 0 with no conflicted path; none of the 14 paths is driver-routed; merge-base unchanged at 70dae533c5; main's commits since touch none of the 14 paths. No governed path in the diff.

① Derived judgments

(a), (c), (d) and (e) carry from 5930680827 unchanged: the code and test blobs are identical, and Test Core, Lint & Repo Gates and TypeScript Type Check are green again on this head.

(b) carries with its FAIL cause removed. The structural half is as before. The behavioural half is now stated correctly in the PR: the Acceptance notes name the Scaffold with repo dist red, its cause (the scaffolded project installs the published @objectstack/spec 17.5.0, which predates the picklists stack key; addbbf0; .changeset/19518-picklist-kind.md unreleased), the repo-build scope of the repro table and chain, the reach (every later create-objectstack PR until the release), and the hold. Each of those claims checks out at the head: .changeset/19518-picklist-kind.md is still present (pending) on 90d55b1e13; #20639's head 718e74c744 (regenerated 12:17Z) has addbbf02ab as an ancestor, its packages/spec/CHANGELOG.md opens with ## 17.6.0 and lists addbbf0: feat(spec): the picklist metadata kind … (#19518) at line 14, and the changeset is consumed there (absent). One immaterial imprecision: the note says the check runs "only … under packages/create-objectstack/**"; the workflow's filter also names docker/** and itself. The out-of-scope README finding from 5930680827 is closed by 90d55b1e13, inside this PR and not on the Tier H item-4 PR, as the remedy asked.

② Semver level

Carries from 5930680827: @objectstack/cli minor, create-objectstack minor, both Clause-②: yes (widening), no major, no BREAKING, no ADR-0087 marker owed; the changeset files are byte-identical and Check Changeset is green on this head. The README commit publishes nothing new that moves the level.

③ Boundary flags

  • Remedy (1), the disclosure: met as written; the Acceptance note is accurate (checked above).
  • Remedy (2), the landing decision: met on the PR. Comment 5931049852 (12:08Z) records the hold — not readied, not queued, no re-run now, until chore: version packages #20639 merges and publishes, then one re-run and a landing if green; the PR is draft: true with auto_merge: null, consistent with it. The card side of that note is not yet reflected at read time: cli + create-objectstack: os generate picklist and the src/picklists scaffold wiring (os init / npm create), after the picklist runtime reader #19519 lands (split from #20825 item 1) #21018 still carries pm:dispatched (no pm:blocked), its body still reads Blocked-by: #19519, and no comment follows the os-dev-report 5930446033. That is the seat's bookkeeping, not the contract; it is flagged so the card and the PR agree.
  • Remedy (3): met by 90d55b1e13, both listings, non-governed, in this PR.
  • The earlier deviations carry (one main merge at 70dae533c5, not re-merged; AGENTS.md not in the diff; the two forced fixture edits; trailers model-free on all four authored commits, the merge commit 5a535158c7 still carrying none). Part of #21018 is the first line, no closing keyword in the body, Part-of PR must not also close its card green. The item-4 line handed over is still accurate to the roster at the head, and skills/objectstack-platform/SKILL.md:194-195 is still the db48028f1a text, owed by the Tier H follow-up.

Landability. The contract is met on this head. The one red is an advisory, non-required check, disclosed in the body and held by the seat, that cannot turn green before the release in #20639 publishes @objectstack/spec 17.6.0 with the picklists key. Once that release publishes and a re-run of Scaffold with repo dist on this head is green, the PR is landable as it stands: required seven green, merge-tree clean against current main, semver and card linkage right, no governed path. The hold is the seat's and is not a verdict input.

Implemented-by: claude/issue-21018-generate-picklist
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 04:28
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 04:28
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit bcd68a2 Oct 2, 2026
39 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21018-generate-picklist branch October 2, 2026 04:54
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…e each decision in words instead of a tracker number (stage 1) (objectstack-ai#21172)

Part of objectstack-ai#20752
Clause-②: no

**Stage 1 of 5 of the `domain:cli` lane under the maintainer's A / A
ruling (5902360492): the CLI, `cloud-connection` and `types` strings.**
The card stays open for stages 2-5, so this PR carries no closing
keyword. Text only: no exit code, error `code`, flag, field, HTTP
status, export or control flow moves.

## What this does

CLI help text, warnings, refusals and two harness errors sent the reader
to a tracker number for the reason behind them. In form D, as the stages
of the engine lane applied it, the number goes. Where the sentence
already said what was decided, only the citation goes. Where it leaned
on the number, it now says the decision in words.

| Where (head line) | Cited | The text now says |
|---|---|---|
| `compile.ts:753`, `validate.ts:689` step line | 3366 | "Checking that
every required capability has a provider installable in this edition..."
|
| `compile.ts:824` warning header | 3786 | "Undeclared authoring keys
(N) — dropped at load; reported here, never refused" (the disposition
commit 8186a70 chose: the schemas stay non-strict, the drop is
reported, never fatal) |
| `db/clean.ts:33` `--database` help | 6469 | "...then the one project
database that os dev, os start and os migrate all resolve" |
| `doctor.ts:1406` retired-key row | 2377 | "`referenceFilters` was
removed from FieldSchema as a key no runtime read (ADR-0049
enforce-or-remove)" |
| `meta/resync.ts:71` skip explanation | 8692 (404) | "on installs
created before the seeder began stamping its default sets 'platform'"
(commit 712e185: the seeder stamps `managed_by: 'platform'`, forward
only) |
| `meta/resync.ts:104` command description | 2705 | "(default permission
sets, which boot seeds insert-once)" |
| `migrate/duplicates.ts:859` description | 8725 | "...index
tightenings, which os migrate plan does not report" (ruling 5377374267:
the report lands here, plan's drift contract untouched) |
| `migrate/duplicates.ts:861` description | 8686 | "Run it BEFORE the
boot-time backfill that stamps untenanted seed rows with the install's
organization" (ruling 5299880350) |
| `migrate/multi-value-columns.ts:332` description | 11535 | "boot and
os migrate plan only report such a column and never alter it unattended"
(route C, recorded in 5405686794) |
| `migrate/recorded-by.ts:69` description | 4556 | "...to NULL, the
value a system-initiated write stores now" (ruling recorded in
5161102937) |
| `migrate/summary-nulls.ts:81` description | 5749 | "...the insert-time
seed, which now writes 0 for an empty child set" (5206990154) |
| `serve.ts:4393` no-auth refusal | 3963 | "anonymous access to object
data is always denied, with no setting that turns that off" |
| `serve.ts:5953` organizations remedy | 4719 | "...deliberately not
enough: accepting it made this wall depend on how the process was
launched." |
| `storage/orphans.ts:173` closing line | 10950 | "Reclaiming these
bytes was decided against: deletes no longer strand files, and files
stranded before that fix are left in place." (ruling 5386673556, closed
not planned; the old line still called it a deferred decision) |
| `test/helpers/serve-process.ts:569` harness error | 13062 | "...the
port `serve.ts` PUBLISHES, which must be the port it BOUND, so this is a
regression" |
| `cloud-connection-route-ledger.ts:205/228/238/248` notes | 8976 |
"`manage_metadata`, never merely a signed-in session" (commit e0695b5
dropped the any-session gate) |
| `cloud-connection-route-ledger.ts:215` note | 9011 (404) |
"`installedBy` / `storageDir` (a user id and a host filesystem path) are
served only to a `manage_metadata` holder" (commit 01074e5) |
| `cloud-connection-route-ledger.ts:264` note | 11863 | "a route and
takes a ledger row, as trigger-api's host-mounted hook route does" |
| `types/src/node.ts:383` undeclared-package note | 10943 (404) |
"...passes its own `fallbackImport`, so the fallback resolves from the
caller instead" (commit 46d34ab) |

Citation only (the sentence already stated the decision): `dev.ts:233`
(5148), `doctor.ts:221` (5673), `doctor.ts:1544` (5397),
`migrate/duplicates.ts:858` (8928: the third sentence already says it
never rewrites), `serve.ts:6021` (4818), `storage-driver.ts:149` and
`:349` (3276), `serve-process.ts:546` (12525), `node.ts:393` (4719).

Every cited card was read (REST, open or closed) before its string was
rewritten. Three answer 404: 8692, 9011 and 10943. They were read
through their landing commits (712e185, 01074e5, 46d34ab) and
today's docblocks.

## Three ledger entries that were not tracker numbers

`serve.ts` carried `objectstack-ai#111`, `objectstack-ai#666` and `objectstack-ai#444`: the 3-digit CSS colours of
the unknown-hostname 404 page, the false-positive family the gate's own
docblock names. They now read `#111111`, `#666666` and `#444444`, the
remedy that docblock prescribes. The page renders the same.

## The source-hash header producer (patch round under claim amendment
`5931899236`)

`packages/cli/src/utils/i18n-extract.ts:2354` held the last two of the
stage's occurrences (`objectstack-ai#12069`, `objectstack-ai#8765`). That literal is the header
`renderSourceHashModule` writes as line 8 of every
`LOCALE.source-hashes.generated.ts`. The seat carried it in this stage,
answer A to the first report's open question.
- **The rewrite, commit `d60b295649`:** the header now states what the
two rulings decided, applied to the generated half. A leaf whose digest
no longer matches its source is stale and serves the source text
instead. The commit sha `09b4f4e4e` stays as provenance.
- **The regeneration, commit `9d5f33f929`:** the 27 companions in 9
packages were regenerated with `node scripts/check-i18n-bundles.mjs
--write`, never by hand.
- The diff has exactly 27 files, one `@@ -8 +8 @@` hunk each, +27 / −27,
with one distinct removed line and one distinct added line.
  - Every hash entry is byte-identical.
- `check:i18n`: 9 packages in sync. `check:i18n-stale-fill`: no new
stale fills.
- **None of the 9 packages publishes the header:** 0 hits for the old or
new header text in each package's `dist`, against a positive control of
2 to 8. So they take no changeset. `@objectstack/cli` ships the literal,
and its existing `patch` changeset covers it.

## Re-pins

- `meta/resync-skip-explanation.test.ts:44` asserted `objectstack-ai#8692`. It now
asserts "before the seeder began stamping its default sets 'platform'".
Ablation on the committed head (`scripts/ablation-replace.mjs`, WRAP
mode): with that phrase replaced in `resync.ts` the file gave 1 failed /
6 passed; after the restore the blob matched HEAD (`a16ba21993`) and
`git diff HEAD` was empty.
- `test/build-json-undeclared-key-parity.e2e.test.ts:288` asserted the
`(objectstack-ai#3786)` header. It now asserts the new header. Ablation: the first
attempt used a replacement that was a substring of the anchor, and the
tool refused it before running anything (count 2 → 2). The second, with
a distinct marker, gave 1 failed / 5 passed. The restore matched HEAD
(`7d24c878b8`) with `git diff HEAD` empty. The docblock transcript at
line 12 keeps the old text, because it records a measurement at
`4ceae8ab0`.
- Neither pin was deleted. The test titles in test files that cite
numbers are not ledgered and were left alone.

## Ledger (`scripts/doc-authoring-prose-id.baseline.json`)

Recomputed with `node scripts/check-doc-authoring.mjs --census-ledger`
(exit 0, no growth refusal) into a scratch file, then copied into place.
The diff deletes 63 lines and adds none. Every row outside the three
packages is byte-identical. After merging `origin/main` (`e35c40a525`)
the recomputed ledger was byte-identical to the committed one.

| | before (`fde553c509`) | after |
|---|---|---|
| `packages/cli` | 29 in 15 files | 0 |
| `packages/cloud-connection` | 6 in 1 file | 0 |
| `packages/types` | 2 in 1 file | 0 |
| **three packages** | **37 in 17 files** | **0** |
| whole ledger | 621 occurrences, 428 pairs, 169 files | 584
occurrences, 395 pairs, 152 files |

`pnpm check:doc-authoring`: before, "521 pinned site(s) across 169
file(s) ... no growth, no burn-down unrecorded"; after the first round,
"488 pinned site(s) across 153 file(s)"; after the patch round, at
`9d5f33f929`, "487 pinned site(s) across 152 file(s) ... no growth, no
burn-down unrecorded". The patch round's ledger diff deletes 4 lines
(the `i18n-extract.ts` row) and adds none.

## Changeset

`.changeset/20752-cli-strings-state-the-decision.md`: `patch` for
`@objectstack/cli` and `@objectstack/types`.

There is **no `@objectstack/cloud-connection` entry**, because the route
ledger is not published. It is package-internal guard data that
`index.ts` does not import. Measured after the build: "never merely a
signed-in session" and `CLOUD_CONNECTION_ROUTE_LEDGER` are each in 0
files under `packages/cloud-connection/dist`, while the positive control
`install-local` is in 6. The same measurement on `@objectstack/cli`:
each new sentence is in `dist`, each old one (`capability providers
(objectstack-ai#3366)`, `dropped at load (objectstack-ai#3786)`, `see issue objectstack-ai#10950`, `always denied
(objectstack-ai#3963)`, `color: objectstack-ai#666;`) is in 0 files. `test/helpers/serve-process.ts`
is not in `dist`.

## Text-only proof

A TypeScript-AST skeleton of each changed `.ts` file compares
`fde553c509` with `2ef3c98a9d`. In the skeleton every string literal and
template text is one placeholder, consecutive literal operands of a `+`
chain merge, and comments are never read. Result: 18 of 18 SAME, with
token and literal-slot counts identical per file. Control: the same tool
reports DIFF on `cli/src/utils/schema-migrate.ts` across `f20f669e17`, a
real code change.

## Tests

- Build: `turbo run build` over `./packages/*` and `./packages/*/*`
under the verify lock, 71/71, before and again after the merge.
- `@objectstack/cli` unit tier: 242 files / 3,435 tests passed, before
and after the merge. `typecheck` exit 0, including
`check:test-typecheck`.
- `@objectstack/cli` integration tier (owed because the diff touches
`test/helpers/serve-process.ts`): 69 files / 599 passed, 1 skipped, on
`2ef3c98a9d`. Not re-run after the merge; the merge brings `start.ts`
and a nightly e2e file, neither touching these strings.
- Nightly tier (`OS_TEST_TIERS=nightly`), the two e2e files that assert
the changed strings: `build-json-undeclared-key-parity.e2e.test.ts` and
`serve-port-readback.e2e.test.ts`, 19/19 passed.
- `@objectstack/types`: 23 files / 692 tests;
`@objectstack/cloud-connection`: 30 files / 397 tests (the route-ledger
conformance guard included); both `typecheck` exit 0.

## Gates

`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` (no paths) at `c023fa0d00` derived 73 commands. All 73 ran
one at a time from the worktree, each exit 0. `--ran` reports "73
derived famil(ies) accounted for — 73 run, 0 NOT-MEASURED". Among them:
`check:doc-authoring` (above), `check:i18n` (9 packages in sync),
`check:issue-citations`, `check:nul-bytes`, `check:dts-closure` (71
packages swept) and `check:dual-build-cjs-loads`.

Narrowed lint: `eslint --no-inline-config --format json` over the 18
changed `.ts` files reported 18 files, 0 errors, 0 warnings (counts from
eslint's JSON). The resolved `parserOptions` are `ecmaVersion: latest,
sourceType: module`, with no `project` or `projectService`, so no
type-aware rule runs and this diff cannot move an untouched file's
verdict. Repo-wide `pnpm lint` is CI's.

NOT MEASURED locally (CI's): the Test Core shards, Dogfood, Build Core
and the workspace type-check lanes. The integration tier was not re-run
on the merge commit.

## Acceptance notes

- **Docs transcripts that quoted the old step line** now show the new
text, byte-identical to the `printStep` argument in `compile.ts` and
`validate.ts`, in the patch round (`d60b295649`):
`content/docs/deployment/cli.mdx:608`,
`content/docs/deployment/validating-metadata.mdx:699` and
`content/docs/ui/react-pages.mdx:393`.
  - `check:docs-transcript-drift` passes.
- PR objectstack-ai#21167 also edits `cli.mdx`, around line 1485 and later, so the
hunks are disjoint.
  - `docs/audits/...` is a dated record and stays.
- `packages/cli/CHANGELOG.md` quotes old lines in released entries. That
file is release-owned and untouched.
- Code comments and docblocks that cite these numbers are not runtime
strings and are unchanged.
- `origin/main` was merged once (`e35c40a525`). It touches
`cli/src/commands/start.ts` and adds a nightly e2e file, and shares no
file with this PR.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… text that says `os plugins` works (objectstack-ai#21306)

Fixes objectstack-ai#21285
Clause-②: no

## What this does

`packages/cli/package.json` listed `@oclif/plugin-help` and
`@oclif/plugin-plugins` under `oclif.plugins`, but both were only
`devDependencies`. oclif loads an `oclif.plugins` entry only when the
same name is in `dependencies`, so neither ever loaded. This PR:

- removes the `oclif.plugins` array (no other `oclif` key changes);
- removes the two `devDependencies` (no consumer remains, see H1) and
regenerates `pnpm-lock.yaml` with `pnpm install --lockfile-only`;
- corrects every published text that described the array or said `os
plugins` works (per-site table below);
- rewrites `test/plugin-commands.test.ts` so it pins the new state
instead of the dead array;
- adds `.changeset/21285-drop-dead-oclif-plugins.md` (`patch`,
`@objectstack/cli`).

Maintainer ruling (verbatim):

> 同意:`oclif.plugins`: 里面那两个插件只装在 devDependencies,所以从来没加载过,`os help` 和
`os plugins` 都不是可用命令。我建议删掉这两条配置。

`packages/spec/**` is untouched. That includes `cli-extension.zod.ts`
and its generated page
`content/docs/references/kernel/cli-extension.mdx`, which still say `os
plugins install`. The spec-lane card objectstack-ai#21286 carries them, and it remains
open.

## Behaviour: nothing an operator sees changes

Reading script: `node packages/cli/bin/run.js` with `NODE_ENV` unset and
`OCLIF_COLUMNS=120`, run from an empty directory. Each run's stdout,
stderr and exit code were captured. The runs:

- `--help`, `help`, `plugins`, `plugins install
@acme/plugin-marketplace` and `frobnicate`;
- `TOPIC --help` for each of the 32 root-level topics;
- an `@oclif/core` `Config.load` dump: loaded plugins, all 65 command
ids and all 75 topics.

That is 114 files per reading.

| Reading | Tree | Result |
|---|---|---|
| before | base `748b24072`, unmodified | `os --help`: 12 topics + 22
commands. `help`, `plugins` and `plugins install` exit 2 with `command
... not found`. Plugins loaded: `@objectstack/cli` only. |
| **positive control** | base, with the two plugins added to
`dependencies` (mutation through `scripts/ablation-replace.mjs`, restore
proven: blob == HEAD, `git diff HEAD` empty) | **differs** in 9 files,
plus 6 new ones. `os help` exits 0. `os plugins` exits 0 ("No plugins
installed."). The command table gains `help` and 10 `plugins:*` ids (65
to 76). The root help gains the `plugins` topic and the `help` and
`plugins` commands. So the reading catches a real difference. |
| after | `cc13e2532` (array and devDependencies removed, lockfile
regenerated) and `a593c8c62` (CLI rebuilt) | `diff -r` against before:
**empty**, all 114 files, same sha256 over the concatenation
(`338e1f0f...5c6f38`) |
| final heads | `3c8442fc0` and `a1e72918c` (after merging main) |
identical, except the two version strings `17.5.0` to `17.6.0`. Those
come from main's Version Packages merge, not from this diff. |

## Hypotheses

- **H1 (no consumer): holds. Both devDependencies are removed.** `git
grep` finds `plugin-help` and `plugin-plugins` only in these places:
  - the array and the `devDependencies` block;
- comments in `bin/run.js`, `doctor.ts` and
`doctor-deprecation-hint-commands.test.ts`;
  - docs text;
  - the one test assertion.
No import, `require`, script, fixture or other importer names them. In
the lockfile, only the `packages/cli` importer referenced them.
- **Lockfile comparison**, measured against both merge bases
(`748b24072` and `5a9292e6f`), with the same result:
- 14 package entries and 14 snapshots are removed and 0 added. Every
removed entry is in the transitive closure of the two plugins:
`@oclif/plugin-help@7.0.2`, `@oclif/plugin-plugins@7.0.3`,
`hosted-git-info@7.0.2`, `isexe@3.1.5`, `lru-cache@10.4.3`,
`npm@11.21.0`, `npm-package-arg@11.0.3`, `npm-run-path@5.3.0`,
`object-treeify@4.0.1`, `path-key@4.0.0`, `proc-log@4.2.0`,
`validate-npm-package-name@5.0.1`, `which@4.0.0` and `yarn@1.22.22`.
  - All 1379 kept snapshots and packages are byte-identical.
- **DOWN count: 0.** Four names lose only a second, plugin-only version:
`isexe` 3.1.5, `lru-cache` 10.4.3, `path-key` 4.0.0 and `which` 4.0.0.
The versions every other consumer resolves are unchanged.
- **H2 (only `dependencies` count): holds.** `@oclif/core` 5.1.2
`lib/config/plugin-loader.js` `loadCorePlugins` calls
`findMatchingDependencies(rootPlugin.pjson.dependencies ?? {},
corePlugins)`. Measured three ways:
  - the before/after identity above;
  - the positive control above;
- a standalone fixture root on 5.1.2: with `@acme/plugin-marketplace`
listed in `oclif.plugins` plus `dependencies`, `marketplace:search`
loads and runs. Moved to `devDependencies`, nothing loads.
- **H3 (the site list is complete): holds, with no new site.** The PM's
grep was re-run, then widened to `plugins
install/uninstall/update/link/...` in space and colon forms, every
`@oclif/plugin-*`, and `os|objectstack plugins|help`. Every hit is
accounted for in the table below. The widened spellings found only three
things beyond the card's sites: `bin/run.js:87` (the `plugins link`
sentence, covered with the run.js site), the phrase "ObjectStack
plugins" (prose, not a command), and one `CHANGELOG.md` line.
- **H4 (the build-your-own-distribution route stays true): holds.**
Decided from the loader, not the old text. Fixture distribution roots on
`@oclif/core` 5.1.2 `Config.load`:
- a root listing `@acme/plugin-marketplace` in both `oclif.plugins` and
`dependencies` loads `marketplace:search` and runs it;
- a root listing both `@objectstack/cli` and the extension that way
loads 66 commands (this CLI's 65 plus `marketplace:search`);
  - devDependencies-only loads nothing.

## Per-site conclusions

| Site | Conclusion |
|---|---|
| `packages/cli/package.json` `oclif.plugins` + 2 `devDependencies` |
**Changed**: removed. |
| `pnpm-lock.yaml` | **Changed**: regenerated by the tooling, comparison
above. |
| `content/docs/plugins/index.mdx` Step 3 callout (lines 400-408) |
**Changed**. Its reason ("`plugin-plugins` sits in devDependencies")
became false. It now says: no plugin manager; `os plugins ...` and `os
help` are not commands; `os --help` is the help entry; the distribution
route, kept because H4 holds; and the loader's `dependencies`-only rule.
|
| same page, "Once loaded, the new commands appear in `os --help`" |
**Already true**: true of the distribution's own `os --help` (H4
fixture). |
| `packages/cli/README.md` `### os plugins (oclif)` | **Changed**. Now
`### os plugins and os help (not commands)`: no plugin manager; each
exits 2; use `os --help`; link to the plugin-system section. |
| `packages/cli/README.md` `## oclif Plugin System` (intro, step 3,
"Install and use", comparison row) | **Changed**. `os plugins install`
is gone. Step 3 and the example load through an `os` distribution
listing the plugin in `oclif.plugins` + `dependencies`. |
| `packages/cli/bin/run.js:84-97` | **Changed**. The reasoning now names
"no plugin manager, no `oclif.plugins`, no `@oclif/plugin-plugins`
dependency" instead of the devDependencies placement. The 34-entry count
is re-measured (12 topics + 22 commands). |
| `packages/cli/src/commands/doctor.ts:2377-2380` | **Changed**
(comment): "no plugin supplies one (the package declares no
`oclif.plugins`)". |
|
`packages/cli/src/commands/doctor-deprecation-hint-commands.test.ts:15`
| **Changed** (comment), same restatement. |
| `packages/cli/test/plugin-commands.test.ts` | **Rewritten** to pin the
new state. `oclif.plugins` is undefined, and no `@oclif/plugin-*`
package appears in any dependency field. The command-discovery and bin
pins are kept. The guard is not deleted (reverse verification below). |
| `docs/qa/platform-checklist/areas/cli.json:658`
(`cli.flag-command-error-ux`) | **Changed**. The source line now reads
"no oclif.plugins at all, no plugin manager, no help command and no
not-found plugin, so unknown commands hard-error". Revision 1 to 2, with
a history entry. `pnpm check:platform-checklist` is green. |
| `packages/spec/src/kernel/cli-extension.zod.ts:18` and
`content/docs/references/kernel/cli-extension.mdx:21` | **Out of
scope**. Spec-lane, carried by objectstack-ai#21286, untouched as the card directs. |
| `packages/cli/CHANGELOG.md:1362` (the "`plugins link`ed TypeScript
plugin ... `@oclif/plugin-plugins` sits in `devDependencies`" entry) |
**Out of scope**. Release-owned, and true of the version it shipped
with. Not edited in a code PR. |
| "ObjectStack plugins" hits (`content/docs/ai/skills.mdx`,
`api/error-handling-server.mdx`, `plugins/development.mdx`,
`packages/core/src/types.ts`, `packages/types/README.md`,
`skills/objectstack-platform/references/plugin-hooks.md`), root
`CHANGELOG.md:1367` | **Not a site**: a case-insensitive match on prose,
not an `os plugins` command. |

## Tests and gates (final head `a1e72918c` unless noted)

- Gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `a1e72918c` derives **106**
commands. All 106 were run with each exit code captured before any pipe,
and **all 106 exit 0**. `--ran` reconciles them: `106 derived famil(ies)
accounted for, 106 run, 0 NOT-MEASURED`.
  - The same 106 were also all green at `3c8442fc0`.
- At `a593c8c62`, 4 needed a re-run. Three refused with exit 3
(`PREREQUISITE NOT MET`) before the packages they read were built:
`check:skill-examples`, `check:dual-build-cjs-loads` and
`check:i18n-coverage`. `check:slot-lookup` hit an ENOENT on a temp
fixture that a concurrent CLI test deleted. All 4 were green on re-run.
- `@objectstack/cli` unit tier (`vitest run --project unit
--maxWorkers=2`): **243 files and 3440 tests passed** at `3c8442fc0`,
and again at `a593c8c62`. The two edited test files were re-run at
`a1e72918c`: 2 files and 16 tests passed. The last merge (`3c8442fc0` to
`a1e72918c`) brought 4 main commits, none of which touch `packages/cli`.

- Reverse verification on the rewritten test, with the fix committed.
Each mutation went through `scripts/ablation-replace.mjs` (anchor hit,
blob changed) with its restore proven (blob == HEAD, `git diff HEAD`
empty):
- re-adding the `oclif.plugins` array: `declares no oclif.plugins` goes
**red** (`expected [ '@oclif/plugin-help', ... ] to be undefined`);
- adding `@oclif/plugin-plugins` to `devDependencies`: `depends on no
@oclif/plugin-* package` goes **red**.
- `pnpm --filter @objectstack/cli typecheck` (at `a593c8c62`): exit 0.
`plugin-commands.test.ts` is in `tsconfig.test.json`'s program and
`doctor-deprecation-hint-commands.test.ts` in `tsconfig.json`'s
(`--listFilesOnly`).
- `--project integration` (at `a593c8c62`, run because the diff touches
`bin/`): 69/70 files and 601/603 tests pass. The 1 failure is
pre-existing: `test/published-entry-node-env-source-reroute.test.ts`,
`CONTROL: neutralising the declaration in the child reproduces the card
verbatim`. It fails identically on base `748b24072`, built (59/59 turbo
cache), in a separate worktree. Cause: tsx 4.23.15's ESM API registers
`./esm/index.mjs` relative to `dist/esm/api/index.cjs`. That resolves to
the nonexistent `dist/esm/api/esm/index.mjs` (`oclif:config:ts-path`
debug: "Could not find tsx. Skipping tsx registration"), so the
control's trap never arms. It is unrelated to `oclif.plugins`. The
integration tier was not re-run at the final head: the incoming main
commits touch no `packages/cli/bin` or `src` file. It is declared to CI.
- Lint, narrowed and measured: `eslint --no-inline-config --format json`
over the 4 touched JS/TS files reports 4 files, 0 errors and 0 warnings.
The other 6 touched files (`.md`, `.mdx`, `.json`, `.yaml`) are outside
`eslint.config.mjs`'s `files` globs. The config enables no type-aware
linting (no `parserOptions.project`), so this diff cannot move an
untouched file's verdict. The full `pnpm lint` is CI's.

## Acceptance notes (not filed here; for the seat)

- `packages/cli/README.md` `### Global` says `-v, --version` and `-h,
--help`. Measured on the built entry: `os -h` and `os -v` exit 2 with
`command -h not found` and `command -v not found`. Only `--help` and
`--version` work. This is pre-existing and unrelated to this diff, so it
is reported, not fixed.
- `packages/cli/README.md` `### Plugin Management` says "There is no `os
plugin` command group in v1". `os plugin build|sign|publish` is
registered: the `plugin` topic is in `os --help`. This is pre-existing,
and objectstack-ai#21167 also holds this file, so it is left untouched.
- The tsx 4.23.15 ESM-API defect above. It reds that integration control
leg on base too, so it will red `packages/cli`'s integration tier
wherever that file runs.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…picklists included (objectstack-ai#21337)

Fixes objectstack-ai#21018
Clause-②: no

This PR is item 4 of objectstack-ai#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 objectstack-ai#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:

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

After:

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

This is the text PR objectstack-ai#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.mjs` reads
`skills/objectstack-platform/SKILL.md is 5830 tokens (ceiling 5833;
headroom 3)` on `82e06107cf`.

## 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 objectstack-ai#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 objectstack-ai#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(代码半,objectstack-ai#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](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… that the built os registers (objectstack-ai#21354)

Fixes objectstack-ai#21310

Clause-②: no

## What changes

`packages/cli/README.md` now says what the built `os` does. Every claim
below was read off the built entry (`packages/cli/bin/run.js`,
`@oclif/core` 5.1.2), run from an empty cwd: `os --help`, `os plugin
--help`, every topic's `--help` and each documented command's `--help`.
The credential sources were also measured against a local echo server.

- **`### Global`** lists `--version` and `--help` only. It says there is
no short form: `os -h` and `os -v` exit 2. It also names the commands
where `-v` is the command's own flag.
- **`### Plugin Management`** drops "There is no `os plugin` command
group in v1". It lists the registered group instead: `os plugin build`,
`os plugin sign` and `os plugin publish`. It notes that the group has no
`install` (per ADR-0025's status line), and that `os plugin` is
unrelated to `os plugins`.
- **Two command-table rows were wrong:** `os init [name]` and `os dev
[package]`. Both are rewritten.
- **Cloud credentials and flags (patch round 1).** The Cloud section
said every cloud command reads `os cloud login`'s session, or `--token`
/ `OS_CLOUD_API_KEY` and `--server` / `OS_CLOUD_URL`. A new `####
Credentials and server URL` table states, per command, the server-URL
flag, the token flag and the stored session it uses. The typical publish
flow now says that its `os environments create` step does not read the
`os cloud login` session.
- **`os serve --ui` (patch round 1)** now uses the `--help` wording:
"Enable the bundled Console portal", in place of "Enable Studio UI".
- **`.changeset/21310-cli-readme-flags.md`** is a `patch` for
`@objectstack/cli`, because `README.md` is in the package's `files`. It
now counts five false claims.

No code, flag, environment variable, exit code or help page changes.
`packages/cli/package.json` is untouched.

## Short flags: the README route (docs follow the implementation)

Readings on the built entry from an empty cwd. "Before" is at
`1caa60373`, the branch point after objectstack-ai#21167 landed. "After" is at
`9bdb092ca`, this head.

| argv | before | after |
|---|---|---|
| `os -h` | exit 2, `command -h not found` | exit 2, `command -h not
found` (unchanged by design) |
| `os -v` | exit 2, `command -v not found` | exit 2, `command -v not
found` (unchanged by design) |
| `os --help` | exit 0, 3712 bytes, md5
`1855676fe5a2bb87aa5871fc1bed196f` | byte-identical, same md5 |
| `os --version` | exit 0, `@objectstack/cli/17.6.0 linux-x64
node-v22.22.0` | same |

**The ruling's check.** The ruling: "show that no command already uses
`-h` / `-v` as its own flag. If one does, choose the README route and
say why."

Six commands already own `-v`. Found by `git grep` for `char: 'v'` in
`packages/cli/src`, then read back in each command's `--help`:

- `-v` is `--verbose` on `os dev` (`dev.ts:212`), `os serve`
(`serve.ts:1209`), `os start` (`start.ts:93`) and `os doctor`
(`doctor.ts:1905`).
- `-v` is `--version VALUE` on `os package publish`
(`package/publish.ts:315`) and `os package install`
(`package/install.ts:57`).

No command owns `-h`. So the ruling sends `-v` down the README route.
`-h` was eligible on its own, and it is dropped too, for the reasons in
the four axes below.

**What the alternative would have done.** These rows come from oclif's
own predicates, `versionAddition` and `helpAddition` in `@oclif/core`
5.1.2 `lib/main.js`. They were evaluated in memory on this package's
loaded `Config`, with `additionalVersionFlags: ["-v"]` and
`additionalHelpFlags: ["-h"]` set on it. No file was written.

| argv | today | with the two keys |
|---|---|---|
| `-v serve` | exit 2, `command -v not found` | version check true:
prints the version, exits 0, **and serve never runs** |
| `serve -v` | `--verbose` | `--verbose` (oclif checks only argv[0] for
a version flag) |
| `serve -h` | exit 2, `Nonexistent flag: -h` | help check true: prints
help |

**The four axes.**

- **实际业务需求.** There is no measured pull. `git grep` for `os -h`, `os -v`
and `objectstack -h|-v` over the whole tree (content/docs, skills,
examples, packages, scripts) finds no occurrence. This README's `###
Global` was the only text that named the short forms.
- **项目长远合理性.** `-v` already has two meanings inside this CLI: verbose on
four commands and a package version on two. Adding a third that applies
only at argv[0] (print the CLI version) makes the flag's meaning depend
on where it appears. Making the docs follow the implementation removes
the false claim with no runtime change.
- **防 AI 写代码犯错.** Today a mistaken `os -v serve` fails loudly with exit
2. With the key set, it would print a version line, exit 0 and start
nothing, so a loud failure would become a silent one. The README now
says the short forms do not exist, so an agent reading it uses `--help`
and `--version`, which work in every position.
- **创业阶段不扩散需求.** New flags would be a new capability with no measured
pull, and the default is to keep scope tight. Making only `-h` work
would also leave `### Global` asymmetric, with no measured user who
needs it.

## `os plugin` — the commands, verbatim

`os plugin --help` at `9bdb092ca`, exit 0. The output is byte-identical
at `1caa60373`.

```text
Compile a plugin into a signed-ready `.osplugin` artifact (ADR-0025 §3.4)

USAGE
  $ os plugin COMMAND

COMMANDS
  plugin build    Compile a plugin into a signed-ready `.osplugin` artifact
                  (ADR-0025 §3.4)
  plugin publish  Publish a signed .osplugin to ObjectStack Cloud (ADR-0025
                  §3.4)
  plugin sign     Sign a built .osplugin with a publisher Ed25519 key (ADR-0025
                  §3.4)
```

Usage lines: `os plugin build [DIR] [-e VALUE] [-o VALUE] [--minify]`,
`os plugin sign ARTIFACT -k VALUE [--key-id VALUE] [-o VALUE]` and `os
plugin publish [ARTIFACT] …`. There is no `install`, and that matches
ADR-0025's status line, which says the code-plugin install half is
unimplemented.


## Every README command-table row against `--help`

Placeholders are spelled in capitals here (TYPE, NAME, ID).

| Section | Row | Conclusion |
|---|---|---|
| Development | `os init [name]` | **Changed.** It said "in the current
directory". `os init --help` says: "When provided, a new directory with
this name is created; otherwise the current directory is used." The
Quick Start's own `os init my-app` was a counterexample. |
| Development | `os dev [package]` | **Changed.** It said "with hot
reload". `os dev --help` says: "watch sources, rebuild the artifact, and
restart the server on change". `dev.ts` records that the old "server
will auto-reload" line "advertised a hot reload the runtime only
partially performs". |
| Development | `os serve [config]` | **Already true.** For "plugin
auto-detection": `serve.ts:11` imports `isHostConfig` /
`shouldBootWithLibrary` from `utils/plugin-detection.ts`, which detect a
host config that carries instantiated plugins. The row leaves out the
artifact fallback that `--help` leads with, but that is an omission, not
a false claim. |
| Build & Validate | `os compile [config]` | **Already true.** `-o`
defaults to `dist/objectstack.json`. |
| Build & Validate | `os validate [config]` | **Already true.** `--help`
also mentions CEL expressions and widget bindings, which the row leaves
out. |
| Build & Validate | `os info [config]` | **Already true.**
`info.ts:117` prints agents. |
| Scaffolding | `os generate TYPE NAME` | **Left alone, per the ruling**
(these rows are objectstack-ai#21167's, which landed as `bcd68a29f` before this
branch's base). It is also already true: `--help` marks NAME optional,
but `generate.ts` refuses a metadata type without a name ("Missing
required argument"). NAME is optional only for the `types`, `client` and
`migration` routes. |
| Scaffolding | `os create TYPE [name]` | **Already true.** `--help`
says "Create a new standalone kernel code plugin from a built-in
template", with TYPE = plugin. |
| Cloud | `os cloud login` | **Already true.** `-e/--email` and
`-p/--password` skip the browser flow, and credentials go to
`~/.objectstack/cloud.json`. |
| Cloud | `os cloud whoami` / `os cloud logout` | **Already true.** Both
are listed in `os cloud --help`. |
| Cloud | `os environments create --org ID --name N` | **Already true.**
Both flags are required in the usage line. There is no `projects` topic
in `os --help`. |
| Cloud | `os environments list` / `show ID` | **Already true.** |
| Cloud | `os package publish [artifact]` | **Already true.** ARTIFACT
defaults to `dist/objectstack.json`. |
| Plugin Management | (prose) | **Changed** (see above). |
| Quality | `os test [files]`, `os doctor`, `os lint [config]`, `os diff
[before] [after]` | **Already true.** The usage lines match. |
| Reference | `os explain [schema]` | **Already true.** |
| CLI Options | `### Global` | **Changed** (see above). |
| CLI Options | `os plugins` and `os help` (not commands) | **Already
true since objectstack-ai#21306; not re-edited.** `os plugins` exits 2 with `command
plugins not found`, and `os help` exits 2 with `command help not found`.
`package.json` has no `oclif.plugins` and no `@oclif/plugin-*`
dependency. |
| Cloud | lead sentence: credentials from `os cloud login`, or `--token`
/ `OS_CLOUD_API_KEY` and `--server` / `OS_CLOUD_URL` | **Changed (patch
round 1).** This holds only for `os package publish` and `os plugin
publish`. The new per-command table is below. |
| Cloud | typical flow: `os cloud login`, then `os environments create`
| **Changed (patch round 1).** With only the `os cloud login` session
present, `os environments create` exits 1 with `Authentication required.
Please run os login or set OS_TOKEN environment variable.` The flow now
says so at that step and names what the step reads instead. |
| Cloud | "Set `OS_CLOUD_URL` (or `--server`)" | **Changed (patch round
1).** `os cloud login`, `os package publish` and `os environments` read
`OS_CLOUD_URL`. The flag is `--server` on `os package publish` and
`--url` on the other two. `os cloud whoami` / `logout` read neither. |
| CLI Options | `### os serve` `--ui` | **Changed (patch round 1).** It
said "Enable Studio UI". It now uses the `--help` wording: "Enable the
bundled Console portal at /_console/ when @object-ui/console is
installed (default: true)". |

## Cloud commands: flags, env vars and stored session, per command

Read off each command's `--help` at `9bdb092ca`. The "stored session"
column was measured, not taken from the help: `HOME` pointed at a temp
dir holding only a `cloud.json`, or only a `credentials.json`, whose URL
was a local echo server that logged each request's path and bearer.

| Command | Server URL flag (env) | Token flag (env) | Stored session it
authenticates with |
|---|---|---|---|
| `os cloud login` | `-u, --url` (`OS_CLOUD_URL`, default
`https://cloud.objectos.ai`) | none: `-e, --email` / `-p, --password`,
or the browser device flow | writes `~/.objectstack/cloud.json` |
| `os cloud whoami`, `os cloud logout` | none (`--json` only) | none |
read / delete `cloud.json` (`cloud/whoami.ts:26`,
`cloud/logout.ts:29,39`) |
| `os package publish` | `-s, --server` (`OS_CLOUD_URL`, default
`https://cloud.objectos.ai`; with neither set, the URL in `cloud.json`)
| `-t, --token` (`OS_CLOUD_API_KEY`, then `OS_TOKEN`) | `cloud.json`.
With only `credentials.json`: exit 1, "Not logged in to ObjectStack
Cloud. Run os cloud login first", and 0 requests. With only
`cloud.json`: the request goes to its URL with its bearer. With
`OS_CLOUD_API_KEY` or `OS_TOKEN`: the request carries that bearer. |
| `os plugin publish` | `-s, --server` (`OS_CLOUD_URL`) | `-t, --token`
(`OS_CLOUD_API_KEY`) | `cloud.json`, by the same precedence code as
package publish (`plugin/publish.ts:170-178`). Code-read only; not run,
because it needs a built `.osplugin`. |
| `os environments list` / `show` / `create` / `bind` / `switch` | `-u,
--url` (`OS_CLOUD_URL`); else the URL in `credentials.json`; else
`http://localhost:3000` | `-t, --token` (`OS_TOKEN`) |
`credentials.json`, the `os login` session. With only `cloud.json`, all
five exit 1 with `Authentication required`, before any request. With
only `credentials.json`, `list` sends `GET /api/v1/cloud/environments`
with its bearer. `OS_TOKEN` works, and `OS_CLOUD_API_KEY` alone does
not. |
| `os package install` (a runtime command, not a cloud one) | `-r,
--runtime` (`OS_RUNTIME_URL`, default `http://localhost:3000`) | none:
`--email` / `--password` (`OS_RUNTIME_EMAIL` / `OS_RUNTIME_PASSWORD`) |
none |
| Also read, not in the README's Cloud section: `os whoami`, `os data
*`, `os meta list/get/register/delete` | `-u, --url` (`OS_CLOUD_URL`) |
`-t, --token` (`OS_TOKEN`) | `credentials.json`, through the same
`createApiClient` (code-read) |
| Also read: `os datasource introspect/list-tables/validate` | `-u,
--url` (`OS_CLOUD_URL`, else `http://localhost:3000`) | `-t, --token`
(`OS_TOKEN`) | none. These use flags and env only
(`datasource/introspect.ts:10-13`, code-read). |
| Also measured: `os login` / `os register` | `-u, --url`
(`OS_RUNTIME_URL` for login, `OS_CLOUD_URL` for register; default
`http://localhost:3000`) | none (email/password, or the device flow for
login) | writes `credentials.json` |

## Acceptance notes

These are out of scope. The last one is filed as objectstack-ai#21360; the others are
not filed.

- **Registered commands with no README row.** `build`, `start`,
`verify`, `login`, `logout`, `register`, `whoami`, `migrate`, `data`,
`datasource`, `db`, `i18n`, `meta`, `secret`, `storage`, `package
install` and `environments bind/switch` have no row. These are
omissions, not mismatches: the README does not claim to be complete, and
`content/docs/deployment/cli.mdx` is the full reference.
- **"Runtime plugins are bundled into the build artifact" is kept as
written.** I did not re-measure it. Only the false clause in front of it
was removed.
- **For the seat — the README is now true, but the flow it documents has
a gap.** After only `os cloud login`, `os environments create` refuses
and says to run `os login`. `os login --help` says "For the hosted
package registry, use `os cloud login` instead." This round changes no
code, so the README states the gap rather than closing it. The seat
filed it as objectstack-ai#21360.

## Verification

- **Build.** `pnpm turbo run build --filter=!@objectstack/docs
--concurrency=2` at `9bdb092ca`: 72/72 tasks, verify-lock `VERDICT
command-exit 0`.
- **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` at `9bdb092ca` gives 52 commands;
the merged `main` added `check-dts-emitted.mjs --self-test`. All 52
exited 0 at `9bdb092ca`, each exit code captured before any pipe. The
`--ran` reconciliation reads "52 derived, 52 run, 0 NOT-MEASURED, 0
UNRUN", and that zero is derived from recorded exit codes.
- **`main` moved again after the last merge.** That happened while the
gates ran: `96b12b589` (a pm-roster step in `lint.yml`) and `23365eaed`
(spec). Neither touches `packages/cli` or this changeset. The merge
queue rebuilds the PR on current `main`.
- **Runtime unchanged.** `os --help` is byte-identical at `9bdb092ca`
and at `1caa60373`: exit 0, 3712 bytes, md5
`1855676fe5a2bb87aa5871fc1bed196f`. `os -h` and `os -v` still exit 2.
- **CLI unit tier.** `pnpm --filter @objectstack/cli exec vitest run
--project unit --maxWorkers=2`: 244 files and 3461 tests passed,
`VERDICT command-exit 0`, at `4e7e91fd2`. Since then, `git diff
4e7e91f 9bdb092 -- packages/cli` touches only
`packages/cli/README.md`, and no CLI test reads that file. The tests
that mention a README read the README that `os create` emits. The
integration tier is left to CI.
- **CLI typecheck.** `pnpm --filter @objectstack/cli typecheck`: exit 0
at `4e7e91fd2`.

---
_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 size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants