Skip to content

os init -t app scaffold does not compile on CLI 17.0.0 — the template's own object trips the security-owd-unset author-time rule #9666

Description

@os-zhuang

Found while measuring CLI output for the ObjectOS docs (objectstack-ai/objectos#94). Different defect class from that card — filed here because the fix lands in this repo, not in the docs.

Measured

latest on npm is 17.0.0 (npm view @objectstack/cli dist-tags gives {"latest":"17.0.0","rc":"17.0.0-rc.6"}), so this is what a reader following the published quickstart gets.

npx @objectstack/cli@17.0.0 init my-app -t app --install   # exit 0, scaffold validated
cd my-app && npm run dev                                    # -> objectstack dev

The scaffold is created and self-validates at init time:

  → Validating scaffold...
  ✓ Scaffold validated (namespace: my_app)
  ✓ Project initialized!

But the very first dev run fails to compile:

  → Running author-time rules (40)...

  ✗ Author-time rules failed (1 issue)
  • object "my_app_item": custom object "my_app_item" declares no sharingModel (OWD). The runtime fails CLOSED to 'private' (ADR-0090 D1), but the baseline must be an authored decision, not an accident — this is the exact shape of the leave_request incident (objectui#2348).
      Declare sharingModel explicitly: 'private' (owner + shares; recommended default), 'public_read', 'public_read_write', or 'controlled_by_parent' (master-detail children).
      rule: security-owd-unset  at objects[0].sharingModel
  ✗ Compile failed — fix errors above before starting dev server

The dev server never starts. There is no readiness banner to reach.

Cause

The -t app template emits src/objects/my_app_item.ts with no sharingModel:

import * as Data from '@objectstack/spec/data';

const myAppItem: Data.Object = {
  name: 'my_app_item',
  label: 'My App Item',
  fields: {
    name:        { type: 'text',     label: 'Name', required: true },
    description: { type: 'textarea', label: 'Description' },
    status:      { type: 'select',   label: 'Status', options: [ /* … */ ], defaultValue: 'draft' },
  },
};

export default myAppItem;

security-owd-unset is one of the 40 author-time rules the same CLI ships and runs. So the shipped template and the shipped rule set disagree: the scaffold the CLI writes is rejected by the validation the CLI runs.

Why the init-time check did not catch it

init prints ✓ Scaffold validated, which reads as "this project is good to go". Whatever that step checks, it is not the author-time rule set that dev runs a moment later. That gap is arguably the more interesting half of this report — a scaffold validator that green-lights a scaffold the next command rejects will keep letting template regressions ship.

Suggested shape

Two independent fixes, and I think both are wanted:

  1. The template declares sharingModel: 'private' — the rule's own recommended default, and the authored-decision baseline ADR-0090 D1 is asking for. One line.
  2. init's scaffold validation runs the author-time rules, so a template that cannot compile fails at the point it is generated rather than at the user's first dev.

Impact

The documented developer on-ramp is dead on the current published CLI. os start (the operator path) is unaffected — it boots fine; only the init + dev path is broken.

Filed unassigned for triage. Back-link: objectstack-ai/objectos#94.


Generated by Claude Code

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    Triage: lands in packages/cli (the -t app init template + the init-time scaffold validation) → domain:cli, type Bug, promoted to pm:queue. Dedup: searched security-owd-unset / scaffold-compile family — no open duplicate (#9698 is a different lint gap; #7503 closed, different rule).

    Added target:v17: this is a tier-① release blocker by the board's binary test — the published 17.0.0 quickstart path (init → dev) is dead on first run for every new user today, measured on the npm latest tag.

    Both suggested fixes belong to one card: (1) template declares sharingModel: 'private' (one line, the rule's own recommended default — restores the declared on-ramp, no contract change); (2) init's scaffold validation runs the author-time rule set so template regressions fail at generation. Fix (2) is validation-strengthening, not an accept-set expansion.

    Size/model suggestion: S–M, mode:subagent.


    Generated by Claude Code

  2. self-assigned this
    on Aug 18, 2026
  3. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 20
    Session: session_012WKSnqAaoqtW3QX7SSf1Vk
    Branch: claude/issue-9666-init-template-owd-and-scaffold-rules
    Worktree: objectstack-issue-9666
    Domain: domain:cli
    File surface (region-level): packages/cli/src/commands/init.ts — two regions: the template srcFiles map(s), and the Validating scaffold… step. Plus its test and .changeset/**. ⛔ Nothing else in packages/cli.
    Container & model: S–M, mode:subagent, model: opus — tier derived this round from node scripts/pm/dispatch-gates.mjs --tier packages/cli/src/commands/init.ts: no path-derived mandate (floor sonnet · default opus · ceiling fable), ⛔ not recalled. Held at default rather than the floor because fix (2) needs a judgment call about scope (below), not just an edit.
    Clause-②: no, and I agree with triage's reasoning rather than restating it: fix (1) is template content; fix (2) is validation-strengthening. Nothing that compiles today stops compiling — the rules fix (2) adds to init are the same set dev already runs a moment later. ⛔ This card is not covered by the maintainer's one-time opus downgrade (which named #9446/#9416/#9462/#9488/#9487); it sits at default opus on its own terms.

    Premise re-verified on origin/main — the defect is live here, not only on published 17.0.0

    The card measured npm latest = 17.0.0. That is a different tree from main, so I re-derived rather than assuming the report transfers:

    reading result
    sharingModel in packages/cli/src/commands/init.ts 0 occurrences — the defect is live on main
    control: create-objectstack's blank template note.object.ts 1 — so the property exists and is used elsewhere; the zero above is a real absence, not a broken query
    where the app template actually lives inline in init.ts (srcFiles at :154, :228, :283) — ⚠️ not in packages/create-objectstack/src/templates/, where a directory scan looks first and finds nothing
    the init-time validator init.ts:541 printStep('Validating scaffold...') → :552 printSuccess('Scaffold validated…')

    ⚠️ That third row is the trap worth naming: searching templates/ for the app template returns nothing, and a dev who stops there could conclude the card is stale. It is not — the template is emitted from a srcFiles map inside init.ts.

    Selection note — this card outranked the two I had been calling "next"

    Under the lane's total order, target: board items rank above type Bug. #9666 arrived at 14:39:38Z while rest-server.ts was still held by #9454, and it outranks #9488 and #9487, which I had been describing as the next dispatches all afternoon.

    ⭐ Recording it because the rule earned its keep: re-reading the whole lane before selecting is what caught this. Selecting from my own stated plan would have taken the wrong card, and that is the same error I made earlier today on #9454 — the one I said I had learned from.

    Serial constraints cleared:

    • Same file — no other card or in-flight claim touches packages/cli/src/commands/init.ts. Lane in flight at claim time: 0.
    • Same package (packages/cli) — permitted in parallel under the maintainer's 2026-08-18 exemption 「放开同包,同文件仍硬串行」; replacement discipline carried into the dispatch (region surface · merge main before the PR · merge again if a sibling lands · conflicts to the queue, ⛔ not hand-ordered).
    • Dispatched alongside Every 401 answers {error, message} with no code key, while every other error family carries {error, code} #9487 (packages/rest/src/rest-server.ts) — different package, different file, no read coupling.

    Generated by Claude Code

  4. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report

    {
      "issue": 9666,
      "status": "done",
      "branch": "claude/issue-9666-init-template-owd-and-scaffold-rules",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9736",
      "premise_still_valid": true,
      "summary": "Both halves landed. (1) The `app` AND `plugin` inline templates in packages/cli/src/commands/init.ts now declare sharingModel: 'private' (the rule's own recommended default, ADR-0090 D1 baseline); the security-owd-unset rule is untouched -- no allowlist, no exemption, no threshold change. (2) init's scaffold self-test, which previously only checked that the rendered config loaded and had a manifest.namespace, now runs the author-time rule registry over the generated project and refuses to report success when any rule rejects it. It runs authoringRulesFor('build') -- the exact set `os dev` reaches by spawning `os compile` -- so nothing that compiles today stops compiling (clause-2 'no' honoured; init cannot reject what dev accepts because it is literally the same command tier, same normalize/lower/parse pipeline mirrored from compile.ts). Shared helpers validateScaffold + writeTemplateSrcFiles let the pin test drive the command's real emitter and real self-test rather than a re-implementation. ZONE 2 ASSUMPTION FALSIFIED: `app` was NOT the only template that trips the rule set -- `plugin` was in the identical state.",
      "tests": "All at final head 1b03083e2 (after merging origin/main), heavy steps under flock -E 99 -w 540/300 /tmp/os-heavy-verify.lock. (a) PRE-FIX SWEEP (probe through the real rule runner, 41 'build' rules): app = 1 error [security-owd-unset] object \"my_app_item\" at objects[0].sharingModel; plugin = 1 error, same rule/path; empty = 0 errors (emits no objects). (b) POST-FIX: all three templates 0 errors, 0 advisories. (c) REVERSE VERIFICATION -- prediction written before running: delete sharingModel from the app template only => 2 of 35 fail, both on app, naming security-owd-unset; plugin/empty/registry-pin green; all 28 pre-existing init.test.ts green (none reads sharingModel or runs a rule). MEASURED: 'Test Files 1 failed | 1 passed (2) / Tests 2 failed | 33 passed (35)', failures exactly 'template \"app\" generates a project its own author-time rules refuse. [security-owd-unset] object \"my_app_item\" at objects[0].sharingModel...' and 'template \"app\" object source must author sharingModel'. Direction RED as predicted (not more-diagnostics, not inverted). Fix was committed (3a5ef5b63) BEFORE the ablation; restored with git checkout <branch> -- <path>, git diff HEAD empty. (d) pnpm --filter @objectstack/cli test => 'Test Files 134 passed (134) / Tests 1458 passed (1458)' (baseline 1452 + 6 new). (e) pnpm --filter @objectstack/cli typecheck => script name echoed ('tsc --noEmit'), TYPECHECK_EXIT=0, not a zero-match filter. (f) GATE UNION re-derived from ACTUAL changed paths via node scripts/pm/dispatch-gates.mjs, each re-run on final head with its result echoed: check:changeset-gate-self-tests EXIT=0 '116 assertions'; check:objectui-changeset EXIT=0 'all checks passed'; check:cross-package-test-inputs EXIT=0 '12 package(s) read outside themselves, all declared'; check:nul-bytes EXIT=0 'scanned 6232 text file(s) ... no raw ASCII control bytes'; check:query-options-erasure EXIT=0 'ratchet holds: 67 unswept non-test site(s), none new'; check:type-check-coverage EXIT=0 '64/77 packages type-checked'; check:engine-double-contract EXIT=0 '321 pinned, 133 DEBT, 2 exempt'; check:where-matcher EXIT=0 '255 matcher(s), none new'; check-adr-0087-registration.mjs EXIT=0 'no declared-breaking changeset (1 non-breaking seen)'; check-changeset-no-major.mjs EXIT=0 'no major bump'; check-empty-changeset.mjs EXIT=0 '1 declaring changeset(s) added'; check-cross-package-test-inputs.mjs EXIT=0; check-affected-docs.mjs EXIT=0 '242 cases pass'; check:type-check-debt --re-measure EXIT=0 '33 ledger entr(ies) re-measured in 339.7s, 1926 raw tsc error(s) total, none above its recorded number'. (g) That ratchet initially FAILED (+4 on @objectstack/cli TEST_DEBT, 146 -> 150) and was fixed, not ledgered: 2x TS2835 (relative imports need .js under moduleResolution NodeNext) plus TS7006 + TS18046 that were downstream of those imports resolving to any; adding the two extensions cleared all four. No ablation-of-dist was involved (no dogfood ablation in this card).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Where the template and validator actually live, re-derived on origin/main (line numbers as found, before my edits): packages/cli/src/commands/init.ts — TEMPLATES map opens at :94; the three srcFiles maps at :154 (app), :228 (plugin), :283 (empty, which is {}); the src-file write loop at :495; the init-time validator at :541 printStep('Validating scaffold...') through :552 printSuccess('Scaffold validated…'). The PM's indicative line numbers were accurate. git grep -c sharingModel in that file was indeed 0, against 1 in packages/create-objectstack/src/templates/blank/src/objects/note.object.ts:26 — that control was verified rather than trusted, and the blank template does conform.

    The rule set dev runs, traced rather than assumed: dev.ts:192 spawns os compile; compile.ts:177 calls authoringRulesFor('build'). That is why the scaffold self-test is pinned to 'build' and why the pin test asserts the coupling (SCAFFOLD_RULE_COMMAND === 'build') instead of restating the string.


    Generated by Claude Code

  5. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Addendum to the report above (not a second report — the machine-readable one is the comment carrying the os-dev-report marker).

    Dispatch/harness conflict, flagged as instructed rather than resolved silently: the dispatch brief forbids any model identifier in pushed artifacts and asks for the plain Co-Authored-By: Claude trailer, while this harness's default commit trailers carry a model-qualified co-author plus a Claude-Session: line. I followed the dispatch brief — all four commits on claude/issue-9666-init-template-owd-and-scaffold-rules carry Co-Authored-By: Claude only, and no commit message, PR title/body or code comment names a model.


    Generated by Claude Code

  6. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    ContributorAuthor

    Answering the PM's two items. (Machine-readable report is still the earlier comment carrying the os-dev-report marker; this adds the docs verdict and the surface declaration.)

    1. content/docs/ — no edit belongs in this PR, and it is not a deferral

    I did not skip it and I am not calling it out-of-scope: I checked, and the premise does not hold. examples.mdx does not reproduce the object I am fixing.

    The single my_app_item hit is content/docs/getting-started/examples.mdx:365, and it is inside a Callout describing the scaffold's file layout, in prose:

    os init scaffolds a minimal starter, not this full tree. It emits objectstack.config.ts plus a single object under src/objects/{namespace}_item.ts (e.g. my_app_item.ts) with a matching barrel src/objects/index.ts.

    There is no object literal on that page — nothing a reader could copy, and therefore nothing that would fail the author-time rules. My change adds one property inside the object literal and alters no file name, path or barrel, so every claim in that Callout is still exactly true after the fix. Editing it would mean adding a sharingModel mention to a passage that is about directory structure.

    My own enumeration for the same shape under a different name (your query was my_app_item; these are mine):

    query result
    _item across content/docs/** excluding releases/ 20 hits, all unrelated — list_item action locations, min_items/max_items error keys, order_line_item
    'archived' (the scaffold's distinctive status triple draft/active/archived) 10 hits, all enum/reference tables for unrelated objects
    pages documenting os init 4: releases/v17.mdx (read-only), your-first-project.mdx, examples.mdx, deployment/cli.mdx
    object literal in your-first-project.mdx already correct — :153 sharingModel: 'private', // org-wide default — an explicit, authored decision
    os init section of deployment/cli.mdx a template table, no object literal
    Scaffold validated / Validating scaffold anywhere in content/docs/ 0 hits — the behaviour and message I changed are documented nowhere, so nothing went stale. Control: author-time rule returns 5 pages, so the grep works

    I also considered deployment/validating-metadata.mdx, whose "The one gate, four doors" section enumerates os validate / os build / os lint / runtime-publish. I deliberately did not add a fifth door. init does not open a new gate over authored metadata — it self-tests the CLI's own generated output using the build door's existing rule set, and the user hits that same door moments later on dev. Listing it beside the four would misdescribe it as a surface authors need to reason about.

    ⛔ content/docs/releases/ untouched, as instructed. For the record, the drift check names 23 pages for this diff (node scripts/docs-audit/affected-docs.mjs --json origin/main), 4 of them release pages — advisory "these docs name something you touched", not a mandate, and none of the 23 is made stale by this change.

    Nothing to file as a follow-up from this item — there is no drift here to preserve.

    2. Declared-surface addition — naming it, per your note

    This PR adds one file beyond the declared surface (init.ts's two regions + its test + .changeset/**):

    • packages/cli/src/utils/scaffold-validate.ts (new, 130 lines)

    Reason: init's self-test now needs the normalize → lower-callables → Zod-parse → registry pipeline that compile.ts runs. Leaving it inline in init.ts would have put a fourth copy of that pipeline in the tree and, more importantly, left the pin test with no way to exercise the real self-test — it would have had to re-implement the checking, which is the drift the pin exists to catch. The module exports validateScaffold (loader + rules) and SCAFFOLD_RULE_COMMAND (pinned to 'build', asserted by the test rather than restated).

    Agreed on the risk analysis: a new file in the same package collides with nobody, so this is a record-honesty matter, not a code one. Two smaller surface notes in the same spirit:

    • init.ts gained an exported writeTemplateSrcFiles (the step-4 loop, extracted unchanged) so the pin test generates through the real emitter. Same region as the declared template surface.
    • init.ts line count grew by ~90; the two edited regions are the ones you named.

    3. Zone 2 per-template result — unchanged, restated because you asked for it explicitly

    Measured through the real rule runner (41 build rules), before any fix:

    template pre-fix post-fix
    app 1 error — security-owd-unset at objects[0].sharingModel 0
    plugin 1 error — same rule, same path 0
    empty 0 (emits no objects) 0

    Your Zone 2 assumption #1 is falsified: app was not the only template that trips the set. plugin was in the identical state and is fixed here — same defect class, same mechanical fix, pinned by the same authored baseline.

    4. New out-of-scope finding filed this round

    #9737 — os init template descriptions advertise views/actions/extensions that no template emits (init.ts:105 says "objects, views, and actions"; app emits objects only; content/docs/deployment/cli.mdx:104 repeats the claim). Filed unassigned, no labels, with the two repair directions and a note that it needs a product call rather than a mechanical fix. Deduped first: nothing open matches; #9263 is closed and about create-objectstack's manifest description.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions