Skip to content

fix(cli): generated scaffolds reach the stack, or os g says they do not (#20215) - #20329

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-20215-generate-scaffolds-reach-stack
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-20215-generate-scaffolds-reach-stack

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20215
Clause-②: no

os init (the app and plugin templates) now wires every barrel os generate writes into, and declares the capabilities the flow scaffold runs on. After writing, os g loads the config again and says whether the new item reached the stack. When the write makes a config that used to load stop loading, os g refuses and takes the write back out. os g never edits a config.

Premise, re-measured on origin/main 6a6a17b6 before any edit

I built the CLI's dependency closure at 6a6a17b6, ran os init my-app -t app --no-install, generated each of the seven types as order_line, and then ran os validate:

step exit what it printed
each os g 0 Tip: Run objectstack validate to check your config
os validate 0 Data: 2 Objects 5 Fields · UI: 0 Apps · Logic: 0 Flows

Row 2 reproduces. With all six barrels wired by hand, os validate exits 1 with "flow 'order_line_flow' declares a 'record_change' trigger but requires does not include 'triggers'". Adding requires: ['triggers'] gives exit 0, UI: 1 Apps 1 Views 1 Dashboards 1 Actions, Logic: 1 Flows.

Booting that hand-wired project with os serve --dev measured two more facts:

  1. The view scaffold is refused at boot. The server said "Invalid views: container from manifest 'com.example.my-app': the container's own name is 'order_line', which disagrees with the object key it binds to, 'my_app_order_line' … drop name, or set it to 'my_app_order_line'". os validate had passed it. So wiring src/views alone would have turned "the view is silently absent" into "the server does not boot" on the road's next step.
  2. triggers is not enough for a flow to run. With requires: ['triggers'] the server booted and printed "Flows: 1 flow(s) declared but the automation engine is not enabled — they will never run. Add requires: ['automation', 'triggers']". Each trigger plugin logged "automation service not available — … NOT installed". With both tokens it printed Flows: 1 flow(s) 1 bound to triggers (record_change, schedule, time_relative, api) · 1 draft.

Row 1: the route, measured

The route is: os init imports every generator's barrel, and os g reports whether its file reached the stack without ever editing the config.

The floor is exact for every config shape. After writing, os g loads the config through the same loadConfig that os validate uses, folds it the way the counter does (authoringRuleUnionStack), and looks for the item's metadata name under the stack key (singularToPlural(type)). It never parses the config's text, so a reordered config, variables, .mjs and packages[] are all read the same way.

os g object, os g view and os g flow (order_line) were run in each shape. The config hash is sha1, taken before and after the three runs:

config shape config hash before → after what os g view / os g flow said os validate
(a) fresh os init -t app fbea6b0e → fbea6b0e reaches the stack, both exit 0, 1 Views, 1 Flows
(b1) hand-edited: imports and keys reordered, an extra import unchanged reaches, both exit 0, counted
(b2) hand-edited: defineStack fed from variables (const ui = {…}; const stack = {…, ...ui}) unchanged reaches, both exit 0, counted
(b3) the pre-fix os init config (./src/objects only) unchanged not wired: prints the import and the key (and requires for the flow) exit 0, 0 Apps, 0 Flows
(b4) the same config as objectstack.config.mjs unchanged reaches, both exit 0, counted
(b5) the create-objectstack blank shape (./src/objects/index.js, requires: ['automation']) unchanged not wired; the flow advice prints the whole list requires: ['automation', 'triggers'], exit 0, 0 Flows
(c) no config n/a not wired: no config here, prints the lines n/a
(d) fresh os init -t plugin unchanged reaches, both exit 0, counted

These rows were measured on cae468f49. The wiring-advice text in (b5) is from c21f96460.

Why not "os g edits the config". That route would be a config editor, a capability the CLI has nowhere today: os init only ever writes a fresh config, and no command rewrites one. Its safety would rest on a recognizer for the author's file. For example, shape (b2) has no defineStack object literal to insert a key into, so an editor must detect it and fall back to the message. The route above changes no config byte in any shape, and needs no editor. That is why this is not a needs_decision. The editor route was not built, so its column is analysis, NOT MEASURED.

Empty barrels. os init writes an index.ts containing only export {}; for each directory the template puts nothing in, and never overwrites an existing one (keyed by renderer, so the objects barrel keeps its old write). The empty barrels must not break the build or typecheck:

  • os validate exits 0 on a fresh project: UI: 0 Apps, Logic: 0 Flows.
  • os compile exits 0.
  • The emitted project's own tsc --noEmit exits 0, measured by the existing scaffold-emission-typechecks.test.ts, which went red on the first version of this change. It led to one design change, described in the next paragraph.

exportsOf, not Object.values. Object.values(emptyBarrel) does not type-check against defineStack for the keys that also accept a name-keyed map. With no export to infer from, TypeScript takes the element type from the map branch, whose name is optional. Measured: TS2322 on actions, flows, dashboards and apps of a fresh project, while views and skills, which have no map form, passed. Three alternatives were measured and all still failed: a spread, Array.from, and .flat(). The template therefore declares one local helper, exportsOf, whose element type comes from the barrel alone: an empty list while the barrel exports nothing, and the exported type once it does. Both states type-check with 0 errors, and a populated barrel is checked exactly as strictly as before.

Prefixed names survive. Object names still go through objectNameFor. The reach check looks for exactly the name the scaffold writes (itemName, held equal to the emitted name by a pin, with and without a namespace).

Row 2: the template declares what the flow needs

Every template that wires src/flows declares requires: ['automation', 'triggers']. The list is derived as the union of the generators' own requires, which today is the flow scaffold's pair. The flow scaffold's header also states the pair.

I chose this over "os g flow adds triggers to requires" because adding to requires is the same config editor. It includes automation as well because of the boot measurement above: without it, the flow validates and never runs.

The one cost is that a fresh project that never holds a flow still mounts the automation engine and the trigger plugins. The config comment says both tokens can go if the project will never hold a flow.

Where the stack carries a flow but lacks a token, os g flow warns and prints the whole requires list to use.

What os g says now

verdict when exit what is left on disk
reaches the stack the loaded stack carries the item 0 the scaffold and the barrel line
cannot run reached, and the stack's requires lacks a token the scaffold runs on 0 as above; the whole requires list is printed
not wired the config loads and does not carry it, or there is no config 0 as above, the config untouched; the import and the key are printed
refused the config loaded before the write and does not load after it 1 nothing: the scaffold, the barrel line and any directory this run created are removed, and the tree is byte-identical
cannot tell the config did not load before the write either 0 as before this change; the verdict says it cannot tell

"Refused" is what the wired barrels make reachable. Measured on c21f96460 in a fresh project:

  • os g action approve without an approve object: exit 1 with defineStack's own "Action 'approve' references object 'my_app_approve' which is not defined in objects", tree unchanged.
  • os g app crm: exit 1, tree unchanged.
  • os g flow into a wired config without requires: exit 1, tree unchanged.

The "cannot tell" row keeps the #20197 control: in a config that does not load, os g dashboard sales still generates, exit 0.

Two fixes in generate.ts, same class, in place

Both are the card's defect class, a scaffold that never reaches the stack or is refused once it does. Both are mechanical, both sit in this claim's file, and both are covered by this card's gates.

  • The view container's name is its object key, prefix included. The server registers a views container under that key and refuses one whose name disagrees. The #20197 census pinned the view's own name as unprefixed because no os validate gate judged it; that assertion is updated, and the reason is written into the pin.
  • Barrel membership is asked of the compiler (barrelExportsBinding), not by indexContent.includes(binding). Measured: after os g view order_line, os g view order found order inside orderLine and exported nothing. The new export {}; barrels would have made that bite os g dashboard port.

Docs

content/docs/deployment/cli.mdx, os generate section:

  • The four verdicts, and that os g never edits the config.
  • A Collected as column in the types table.
  • A view's name is its object key.
  • The example block now binds every scaffold to the object it generates first. os g action approve / os g app crm would now be refused in an os init project.

"Typical Workflow": step 3 is now os g flow opportunity. As written, os g flow lead_qualification now counted (1 Flows) but os validate warned the flow "targets object 'my_crm_lead_qualification', which this stack does not define … the flow will never fire". With opportunity, only the draft-status advisory remains. Step 4 ("Validate everything") is true as written: measured on 4173b2067, exit 0, 4 Objects, 1 Flows.

Changeset

.changeset/20215-generate-scaffolds-reach-stack.md is a patch for @objectstack/cli. It is a bug fix in a released package, Clause-②: no as claimed, the same shape #20197 landed its os g refusals under. It states what os init and os g now write and say that they did not before. The pending namespace-prefix note this PR falsified is corrected in place instead (next section), so this changeset carries no supersession paragraph.

A pending release note corrected in place (DELIBERATE CORRECTION)

This PR rewrites two sentences of .changeset/20197-generate-object-namespace-prefix.md, another card's PENDING release note. This PR makes both sentences false, and both notes compile into the same release. Commit a03756d5e carries that correction alone. Commit da4aca641 then drops the supersession paragraph this PR's own changeset carried, because the sentences it pointed at no longer exist.

node scripts/check-empty-changeset.mjs --base origin/main is red on this PR by design. It names that one file, "present on the merge base and CHANGED by this PR", and this is its DELIBERATE CORRECTION class: "your change may have made this PENDING release note false, and you rewrote it in the same stroke. Remedy: do NOT restore it -- say so on the PR and get it confirmed; restoring it from the base would put the false sentence back." The confirmation is the same-head contract review (seat answer 5860440515; claim 5859284846 amended to name this file). The precedent is PR #20284.

Line 11, One namespace source., last sentence:

  • Before: "dashboard and skill scaffolds name no object and never read the config."
  • After: "dashboard and skill scaffolds name no object, so a config that does not load does not stop them, but os g loads the config after every write, theirs included, to report whether the scaffold reaches the stack."

Line 12, Unchanged:, second sentence:

  • Before: "A view's, action's, flow's, dashboard's, app's and skill's own name, and an action's flow target, are written as before."
  • After: "An action's, flow's, dashboard's, app's and skill's own name, and an action's flow target, are written as before; a view's own name now equals the object key it binds to, prefix included."

Nothing else in that file moved: git diff --word-diff of a03756d5e shows these two sentences only (2 insertions, 2 deletions).

Readings for this round on head eedad4d37, after merging origin/main a78f731ad:

  • check-empty-changeset exit 1, naming only the file above.
  • The 95 derived families all ran. --ran reports "95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN", and every exit is 0 except that one.
  • pnpm lint exit 0.
  • node scripts/check-issue-citations.mjs --base origin/main exit 0 (18 citations resolve).
  • CLI typecheck and the five per-PR scaffold pins: 81/81.
  • The nightly chains: 17/17.

Pins

  • packages/cli/test/generate-scaffold-wiring.test.ts (unit, per-PR) covers:
    • the roster (every stack key is a key the stack schema declares; itemName is the emitted name);
    • the app/plugin templates (each barrel imported, wired, written; requires declared; the emitted project loads with every key a list);
    • barrel membership, the reach reader and the wiring lines;
    • os init keeping an author's barrel.
  • packages/cli/test/generate-stack-reach.test.ts (spawns the CLI, integration tier, per-PR, NOT .e2e) covers:
    • "refused", with the tree byte-identical, for an action with no object and a flow in a requires-less stack, plus a control where the same action generates once the object exists;
    • "not wired", for a pre-fix config (config byte-identical) and for no config;
    • "cannot run";
    • os g dashboard port against the export {}; barrel.
  • packages/cli/test/generate-scaffolds-reach-stack.e2e.test.ts (nightly) is triage's pin: os init -t app, then os g of every type, then os validate exits 0 with 2 Objects, 1 Apps, 1 Views, 1 Dashboards, 1 Actions, 1 Flows. os compile's artifact carries every generated item, the skill included (os validate's summary has no skills row).

Verification

Round 0 readings, on head e33889d77 unless noted (patch round 1's readings on eedad4d37 are in the DELIBERATE CORRECTION section):

  • pnpm --filter @objectstack/cli typecheck (tsc plus the test layer): exit 0.
  • pnpm lint (whole repo, not narrowed): exit 0.
  • CLI unit project: 230 files, 3296 tests, all pass on 01a556a52 (after merging origin/main). The only later commit touches one integration-tier test file.
  • CLI integration project, in two batches: 58 files, 485 pass, 1 skipped (not in a file this PR touches), on 01a556a52. generate-stack-reach.test.ts passes 7/7 on e33889d77.
  • OS_TEST_TIERS=nightly, generate-scaffolds-reach-stack.e2e.test.ts plus the existing generate-object-namespace-prefix.e2e.test.ts: 17/17.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands): 94 families, each run with its exit code recorded. --ran reports "94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN", all exit 0. On 01a556a52, three gates first refused with exit 3 (a prerequisite: packages outside the CLI closure had no dist). They were re-run to exit 0 after building.
  • node scripts/check-issue-citations.mjs --base origin/main: exit 0 (27 citations resolve).
  • Merged origin/main at 6ac33a57d (which carries PR feat(spec,metadata-protocol): a stored filter the record-filter conversion leaves as stored is a TODO in os migrate meta --stored, not silence (#17321) #20244's cli.mdx edit, a disjoint range), with a clean merge. The five commits origin/main gained since touch no packages/cli or cli.mdx path.

Ablations

Each ablation was committed first, mutated through scripts/ablation-replace.mjs (the anchor must hit, and the landing is proven by blob hash), run, and restored. Every restore was proven: the blob equals HEAD's (init.ts 3770e16c, generate.ts 3a92cfa4) and git diff HEAD is empty. All four ran on e33889d77, and every direction was red.

mutation per-PR guards nightly chain
revert the wiring (the templates wire objects only) 9 failed 8 failed (every os g prints wiring lines; counts; artifact)
revert row 2 (delete the template's requires line) 4 failed 3 failed (os g flow refused; counts; artifact)
view name back to the unprefixed stem 3 failed (incl. the #20197 census) n/a
barrel check back to includes 1 failed (os g dashboard port) n/a

Acceptance notes

  • The npm create objectstack starter is not wired. packages/create-objectstack/src/templates/blank/objectstack.config.ts imports ./src/objects/index.js alone and declares requires: ['automation']. It is read-only for this card. On that road (the north-star road starts there), os g view now says "not wired" with the lines, and os validate still counts 0 until the starter wires its barrels. Reported, not edited.
  • packages/spec/prompts/create-new-project.md (read-only here) lists flows/, dashboards/ and reports/ in its project tree, but its config sample wires objects, actions and apps only.
  • cli.mdx about line 741 (the "Your First App" fixture callout, outside this claim's ranges) says the walkthrough's os generate commands would make the summary gain "my_app_customer and a Logic: row". In the create-objectstack starter those scaffolds are not wired, and os generate action approve binds to no declared object.
  • The pending .changeset/20197-generate-object-namespace-prefix.md had two sentences this PR makes false. On the seat's answer (A), they are corrected in place, as the DELIBERATE CORRECTION section above describes; check-empty-changeset stays red for that class by design.
  • os validate's summary counts no skills (collectMetadataStats has no skills member), so the chain pin holds the skill through the compiled artifact.
  • Hand-wiring an empty barrel with Object.values hits TS2322 for the map-supported keys. The cause is MetadataCollectionInput's map branch in packages/spec, read-only here. The template avoids it with exportsOf.
  • Two runtime/validate disagreements the boot measurement surfaced are handed to the seat in the dev report rather than fixed here:
    • os validate passes a views container whose name disagrees with its object key, which os serve refuses;
    • defineStack's trigger-capability rule accepts triggers without automation, and the server then never runs the flow.

Generated by Claude Code

os init's app and plugin configs now import every barrel os generate
writes into (derived from the generator roster) and declare the
capabilities the flow scaffold needs; os g loads the config after writing
and reports whether the item reached the stack, refusing and rolling back
a write that makes a loading config stop loading. The view scaffold's
container name now equals the object key it binds to, and barrel
membership is asked of the compiler instead of a substring test.

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
…to-validate chain

The init templates read barrels through a typed exportsOf helper: with an
empty barrel, Object.values took its element type from defineStack's map
branch and a fresh project failed its own tsc.

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
…efore the control

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
…hat validates what it generated

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

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 31 documentable anchor(s).

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json a78f731add67eab50b3e969e8ad46e44d195ab12.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectstack.config.ts (literal, 32 pages)
  • 4 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 — 25 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 a78f731add67eab50b3e969e8ad46e44d195ab12 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1da143eef47aea1eb9398c58c72abd1a4270e9ff — the merge of head eedad4d37ccf16ab1d28d8c3ba07e80175f20894 into base a78f731add67eab50b3e969e8ad46e44d195ab12, 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 1da143eef47aea1eb9398c58c72abd1a4270e9ff && git checkout 1da143eef47aea1eb9398c58c72abd1a4270e9ff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a78f731add67eab50b3e969e8ad46e44d195ab12 eedad4d37ccf16ab1d28d8c3ba07e80175f20894 && git checkout -B drift-repro a78f731add67eab50b3e969e8ad46e44d195ab12 && git merge --no-ff eedad4d37ccf16ab1d28d8c3ba07e80175f20894

node scripts/docs-audit/affected-docs.mjs --json a78f731add67eab50b3e969e8ad46e44d195ab12

⚠️ 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 a78f731add67eab50b3e969e8ad46e44d195ab12 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…refix note this PR falsifies

The pending note said dashboard and skill scaffolds never read the config,
and that a view's own name is written as before. With this PR os g loads
the config after every write to report whether the scaffold reaches the
stack, and a view's name equals the object key it binds to. Corrected in
place (the DELIBERATE CORRECTION class of check-empty-changeset.mjs), both
notes compiling into the same release.

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
…ce-prefix note is corrected in place

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: eedad4d37ccf16ab1d28d8c3ba07e80175f20894

① Derived judgments

  • The route (the templates wire every barrel; os g never edits the config). Correct.
    • It is the right reading of triage 5855744374: 「the claimant chooses between wiring the barrel on os g and importing every barrel in the templates; measure which keeps a hand-edited config safe; a loud not-wired line is the floor」.
    • init.ts SCAFFOLD_WIRED_BARRELS is derived from GENERATOR_SCAFFOLD_TARGETS, and both the app and plugin configContent render renderWiredImports() + renderWiredStackKeys().
    • generate.ts runMetadataGeneration only calls measureStackReach (utils/scaffold-wiring.ts), which runs findConfigPath + loadConfig + authoringRuleUnionStack, the same fold that validate.ts:350 counts with. It never touches config text.
    • Driven from the head's source in a scratch worktree (tsx bin/run-dev.js):
      • a fresh os init my-app -t app --no-install;
      • os g object|view|action|flow|dashboard|app|skill order_line: every run exits 0, printing Reaches the stack: … carries it in \KEY` as 'NAME'`;
      • config sha1 716a96aa before and after;
      • os validate exits 0 with Data: 2 Objects, UI: 1 Apps 1 Views 1 Dashboards 1 Actions, Logic: 1 Flows;
      • the os compile artifact carries all seven names, including skills: ['order_line'].
  • Hand-edited configs: honest for every claimed shape. Driven:
    • (b3) an objects-only pre-fix config: os g view/flow exit 0 with Not wired, printing import * as views from './src/views'; / views: Object.values(views), (the flow also prints requires: ['automation', 'triggers'],). The config sha is unchanged, and validate counts 0 Apps/0 Flows.
    • (b2) reordered imports, an extra node:path import, and defineStack fed from spread variables: Reaches, the sha is unchanged, and validate counts 1 Views 1 Flows.
    • (b4) objectstack.config.mjs: Reaches, the sha is unchanged, and the items are counted.
    • (c) no config: Not wired: there is no objectstack.config.{ts,js,mjs} here, the lines are printed, and the file is written.
    • (d) the plugin template: Reaches, the sha is unchanged, and validate counts 1 Views/1 Flows.
    • Triggers-only requires: Reaches, plus It will not run yet … requires: ['triggers', 'automation'],.
    • A broken config: os g dashboard generates and exits 0 with cannot be told; os g view is refused (the [finding] os generate object NAME in an os init -t app project writes name: 'NAME' with no namespace prefix, so the next os validate refuses the object the CLI just generated #20197 control).
  • Byte-identity on refusal. Correct.
    • os g action approve into a wired project, and os g flow into a requires-less wired project, both exit 1, and the full-tree sha1 listing is identical before and after. generate.ts records createdDir, barrelBefore and barrelWritten, and unwinds them in the refusal branch.
    • The barrel line is only ever appended AFTER the scaffold's writeFileSync, so "barrel updated, scaffold missing" cannot arise from the refusal path.
    • The plain I/O catch (printError + exit 1) does not unwind, but that shape is pre-existing on main.
  • Row 2. Correct.
    • requires: ['automation', 'triggers'] is SCAFFOLD_WIRED_REQUIRES, the union of GENERATORS.flow.requires (FLOW_SCAFFOLD_REQUIRES).
    • stack.zod.ts validateTriggerCapability refuses a record_change flow without triggers. An absent requires counts as omitting it, which the exit-1 drive above confirms.
    • format.ts:1285 is the boot warning for flows without automation.
    • Both tokens are in PLATFORM_CAPABILITY_TOKENS.
    • Nothing to install:
      • capability-preflight.ts makeProviderResolver resolves from the host dir OR the CLI's own module graph (createRequire(import.meta.url)), and @objectstack/service-automation and @objectstack/trigger-record-change are @objectstack/cli dependencies.
      • Measured: os validate printed "Capability … not installed" only while those CLI deps had no dist in my partial build, and went silent once they were built.
    • The cost (a flow-less project mounts automation + triggers) is stated in the template comment.
  • exportsOf and export {};. Correct.
    • A fresh os init -t app project passes tsc --noEmit -p . with exit 0.
    • Ablating exportsOf(x) → Object.values(x) in the emitted config gives exactly 4× TS2322, at the actions/flows/dashboards/apps lines (the MAP_SUPPORTED_FIELDS keys, metadata-collection.zod.ts:61). That reproduces the PR's stated measurement. The populated project also typechecks.
    • Empty barrels:
      • generate-scaffold-wiring.test.ts asserts that every non-object key loads as [];
      • scaffold-emission-typechecks.test.ts, which runs all templates through their own tsconfig, passed here 41/41 with the modified pins.
    • stackKey = singularToPlural(type):
      • PLURAL_TO_SINGULAR maps views/actions/flows/dashboards/apps/objects/skills one-to-one, with no duplicate singular, so Object.fromEntries cannot shadow;
      • the roster pin checks each key against ObjectStackDefinitionSchema.shape.
  • The two extra generate.ts fixes, against the bounded in-place exemption. .claude/agents/os-dev.md §3 sets four conditions: ① the same defect class, ② mechanical with a pinned shape, ③ no other claim on the file, and ④ the same gate family. It also owes the claim-surface amendment and a mention in the PR body.
    • View name: ObjectQL.registerMetadataCollections (engine.ts:6480-6510) throws when a container's name differs from the key derived from object. So in a namespaced project, the old scaffold is refused at boot exactly once src/views is wired, which is row 2's class.
      • The fix is objectNameFor(name, namespace), the derivation already used for object:. It is pinned by the census ('view.name (its object key)': PREFIXED) and by the roster pin.
      • Nothing reads the old name:
        • the runtime always registered the container under the derived key (toRegister = … {...item, name: itemName});
        • getViewsByObject filters expanded items by object;
        • ViewSchema.name is grammar-free;
        • no doc sample shows the old name.
    • Barrel membership: on main, it was indexContent.includes(toCamelCase(name)). Measured on the head, os g view order after order_line now appends export { default as order }. barrelExportNames walks named exports only.
    • Claim 5859284846 was amended in place (updated 22:30Z) to name both fixes, and the PR body names them with evidence. The conditions hold, and both fixes are correct.
  • The pins.
    • Run on the head: generate-scaffold-wiring.test.ts (unit tier) and generate-stack-reach.test.ts (a spawn file that vitest-tiers classifies as integration, per-PR) → 40/40. The .e2e chain is nightly by name.
    • The read-only fence forbids mutating the checkout, so each ablation was read against the pins rather than run:
    • Observation, not a defect: only the nightly chain proves that os validate counts. No per-PR pin invokes os validate. The per-PR spawn pin covers the reach reader through the same fold, and I measured the count directly above.

② Semver level

@objectstack/cli patch, Clause-②: no, no arm: correct.

  • No key is added to any published payload, and no published accept set moves:
    • packages/cli/src/index.ts is untouched; it re-exports only the command classes;
    • TEMPLATES and SCAFFOLD_WIRED_* are unreachable from the exports map, so per contract-review.md they are shipped bytes, not a published surface;
    • packages/spec is not touched.
  • The scaffold-output changes (a prefixed view name, new barrels, requires) are generated project content.
  • The new exit 1 (a write that stops a loading config from loading) is the same class as [finding] os generate object NAME in an os init -t app project writes name: 'NAME' with no namespace prefix, so the next os validate refuses the object the CLI just generated #20197's os g refusals, which shipped as patch with no arm. It is not the minor (narrowing) class of .changeset/19120-* / 19417-*, which shrank the accept set of a published API door (POST /api/v1/packages) and of the protocol install primitive.
  • The WHICH LEVEL rule (pr-automation.yml) raises to minor only for a widening of a public surface, which does not occur here.

③ Boundary flags

  • Deliberate correction of .changeset/20197-generate-object-namespace-prefix.md. Commit a03756d5e is a word-diff of 2 insertions and 2 deletions, and nothing else in the file moved (verified).

    • Line 11, last sentence.
      • Before: "dashboard and skill scaffolds name no object and never read the config."
      • After: "dashboard and skill scaffolds name no object, so a config that does not load does not stop them, but os g loads the config after every write, theirs included, to report whether the scaffold reaches the stack."
      • (a) Made false by THIS PR: yes. runMetadataGeneration now calls readProjectNamespace() unconditionally, and measureStackReach after every write.
      • (b) True of the combined release: yes, measured. os g dashboard sales into a throwing config generates (exit 0) and prints does not load, so whether … reaches its stack cannot be told. A wired project prints Reaches the stack for dashboard and skill.
      • (c) See below.
    • Line 12, second sentence.
      • Before: "A view's, action's, flow's, dashboard's, app's and skill's own name, and an action's flow target, are written as before."
      • After: "An action's, flow's, dashboard's, app's and skill's own name, and an action's flow target, are written as before; a view's own name now equals the object key it binds to, prefix included."
      • (a) Made false by THIS PR: yes. The view generator's name moved from toSnakeCase(name) to objectNameFor(name, namespace).
      • (b) True of the combined release: yes. The diff touches no other scaffold's name and not the action target (all itemNames are pinned equal to the emitted names), and the drive shows views carrying my_app_order_line.
    • (c) Nothing else in the note is now false. The prefix, no-double-prefix, one-source (the refusal on a non-loading config for object-naming types is retained) and --help sentences all still hold. Line 9's "That covers …" enumeration is now non-exhaustive, since a view's name is also a prefixed object name, but line 12 states it, so it is not false.
    • Deliberate correction of .changeset/20197-generate-object-namespace-prefix.md confirmed: 2 sentence(s) judged.
  • The PR's own changeset after dropping the supersession paragraph (da4aca641, 2 deletions): complete and true. Every remaining statement matches the diff and the drive:

    • the wired imports + exportsOf;
    • export {}; barrels, never overwritten;
    • the requires pair, with its two failure modes;
    • the four os g outcomes;
    • refusal + unwind;
    • dashboard and skill reading the config;
    • the view name;
    • the compiler-asked barrel step;
    • the flow header;
    • earlier projects keeping their config.

    Nit only: the barrel is export {}; plus a four-line comment, while the changeset and PR body say "containing only export {};".

  • Files outside the amended claim's surface: none. The 11 PR files are:

    • init.ts and generate.ts;
    • the new utils/scaffold-wiring.ts, a helper the wiring needs;
    • five packages/cli/test/* files;
    • cli.mdx, at the os generate section (:1403-:1470) and Typical Workflow (:2197-:2205, which is the :2156-:2175 range shifted);
    • .changeset/20215-*.md;
    • the 20197 note the amended claim names.
  • PR body vs diff: the route table, the refusal rows, the "cannot tell" control, the exportsOf TS2322 measurement, the view and barrel fixes, the docs list and the ablation table all correspond to the diff and to what I measured. The (b5) create-objectstack blank reading is consistent with wiringLines printing the whole requires list. Clause-②: no is on its own line, as check-changeset-no-major.mjs requires.

  • CI on head eedad4d37: 43 check-runs, all completed.

Implemented-by: claude/issue-20215-generate-scaffolds-reach-stack
Reviewed-by: session_01UYBdGBzWSrAMzpW8ah3GbP

Independence: INDEPENDENT AGENT (fed the card, the triage direction and the PR only; not the dispatch order or the seat's conclusions)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Check Changeset is red by design on this head: a confirmed DELIBERATE CORRECTION

domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · written 2026-09-28T00:30Z

  • The gate: Check Changeset (.github/workflows/pr-automation.yml, job changeset-check), which runs scripts/check-empty-changeset.mjs --base "$MERGE_BASE".

  • The reason: this PR rewrites two sentences of .changeset/20197-generate-object-namespace-prefix.md, a PENDING release note that PR fix(cli): os generate gives object names the manifest namespace prefix #20214 added. This PR makes both sentences false:

    • after every write, os g now loads the config, dashboard and skill included;
    • a view's own name now equals its object key.

    That is the gate's DELIBERATE CORRECTION class: 「do NOT restore it -- say so on the PR and get it confirmed」 and 「this gate stays red either way」.

  • The confirmation: the at-tier contract review on this same head (eedad4d3), record 5861268261, names the note and judges both rewritten sentences: 「Deliberate correction of .changeset/20197-generate-object-namespace-prefix.md confirmed: 2 sentence(s) judged.」 Under landing-operations.md, that record is the confirmation.

  • Why it does not block the queue: pr-automation.yml triggers on pull_request only, with no merge_group, and Check Changeset is not a required context. Every other check on this head is success or a rostered expected skip.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 00:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit c5dcb3b Sep 28, 2026
43 of 45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20215-generate-scaffolds-reach-stack branch September 28, 2026 00:55
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…el, so `os g` scaffolds reach the stack (objectstack-ai#20333) (objectstack-ai#20363)

Fixes objectstack-ai#20333
Clause-②: no

## Summary

`npm create objectstack`'s blank starter imported `./src/objects` alone,
so everything `os g view|action|flow|dashboard|app|skill` wrote was
never loaded and `os validate` counted 0 of it. The starter now wires
the seven generator barrels `os init` wires since PR objectstack-ai#20329, in the
lines `os init` renders: `exportsOf` over `export {};` barrels, and
`requires: ['automation', 'triggers']`. The copy is bound to the CLI's
single source (`SCAFFOLD_WIRED_BARRELS` / `SCAFFOLD_WIRED_REQUIRES`,
derived from `GENERATOR_SCAFFOLD_TARGETS`) by a parity pin, so it is not
a second wiring rule. A per-PR pin drives `npm create objectstack` → `os
g object` (control) → `os g flow` → `os validate` and reads `Logic: 1
Flows`.

## What changed

-
`packages/create-objectstack/src/templates/blank/objectstack.config.ts`:
imports every wired barrel, declares the `exportsOf` helper, and hands
each barrel to its stack key. The objects import changes from
`'./src/objects/index.js'` to `'./src/objects'`, the extensionless form
`os init` renders, which the parity pin compares verbatim; the
template's `moduleResolution: bundler` resolves the directory index, and
a fresh scaffold type-checks. It carries `requires: ['automation',
'triggers']`. `automation` was already there for the three connector
plugins, and its comment keeps that reason.
- Six new `src/{views,actions,flows,dashboards,apps,skills}/index.ts`
barrels, byte-identical to what `os init` writes.
- Two pins in `packages/cli/test/` (below). The CLI is the only package
that can call the renderer, and it already depends on
`create-objectstack`.
- `packages/cli/package.json` gains
`@objectstack/connector-{rest,openapi,mcp}` as devDependencies (lockfile
+9 lines, one importer block). They exist only so the scaffolded project
the chain pin builds under the CLI's `node_modules` can resolve the
blank config's connector imports, and so CI builds them in
`@objectstack/cli#test`'s closure.
- `scripts/cross-package-test-inputs.mjs` and `turbo.json` declare the
blank config and `src/**` as inputs of `@objectstack/cli#test`, with a
witness for the barrel glob the scan cannot name.
- Docs this change made false (see below), and a `create-objectstack`
patch changeset.

## Why a static copy, and what binds it

`create-objectstack` cannot import the roster. The dependency edge runs
the other way, and the npx entry must not pull the CLI's closure: the
boundary `scripts/sync-scaffold-emission-policy.mjs` already documents.
Measured options:

- **Generate at build time.** The roster is computed from the
`GENERATORS` literal in `generate.ts`. Reading it at
`create-objectstack`'s build would need either text-parsing that
literal, or evaluating the CLI's source before the CLI's own
dependencies are built, which is a build-order cycle.
- **Parity pin over a static copy.** Chosen as the least machinery.
`create-objectstack-wiring-parity.test.ts` reads every expected line off
the CLI: the barrel import lines, the `exportsOf` line and the stack-key
lines of `TEMPLATES.app.configContent`, the `requires` tokens as a
superset of `SCAFFOLD_WIRED_REQUIRES`, and each empty barrel byte for
byte from `TEMPLATES.app.srcFiles`. A generator added to the roster, a
renderer change or a hand edit of the template reddens it (ablations A1
to A3).

## Measured before and after, through the real commands

The on-ramp's real `bin/` scaffolded `my-app --skip-install
--skip-skills` into a directory where the config's imports resolve, then
this repo's CLI ran.

| step | `origin/main` `c74de10a9` | this branch |
|:---|:---|:---|
| `os g object order_line` (control) | exit 0, reaches the stack | exit
0, reaches the stack |
| `os g flow order_line` | exit 0, **Not wired** | exit 0, reaches the
stack |
| `os validate` | exit 0, `Data: 2 Objects`, `Logic: 0 Flows` | exit 0,
`Data: 2 Objects`, `Logic: 1 Flows` |

- **`exportsOf` is required here too.** A fresh starter type-checks
(`tsc --noEmit`, 6.0.3, exit 0). The same starter with `Object.values`
on the empty barrels fails with 4 x TS2322 (actions, flows, dashboards,
apps). After generating the object and the flow it still type-checks.
- **`requires` boots.** `os dev --fresh` on a random port: the flow-less
fresh starter was healthy after about 22s, `/api/v1/ready` answered 200,
and `AutomationServicePlugin` and the record-change, schedule,
time-relative and api trigger plugins loaded, resolved through the CLI's
own dependencies. With the generated flow it was healthy after about 24s
and reported `Flows: 1 flow(s) 1 bound to triggers`. Neither boot
printed "not enabled" or "NOT installed".
- **Census.** `src/templates/` holds one starter, `blank`, which is also
the registry's only entry.

## Pins

- `packages/cli/test/create-objectstack-wiring-parity.test.ts` (unit,
per-PR): 20 cases, described above.
- `packages/cli/test/create-objectstack-stack-reach.test.ts`
(integration, per-PR, not `.e2e`): the chain above, with item names read
off the generator roster. It asserts the exit codes, the named subjects,
the absence of the wiring lines and of a `requires` line from `os g
flow`, and the `Data: 2 Objects` / `Logic: 1 Flows` counts. No prose is
pinned.

## Ablations

Each ran after the fix was committed. Mutations went through
`scripts/ablation-replace.mjs` in wrap mode, which verified the anchor
count and the blob change and restored with blob equal to HEAD and an
empty `git diff HEAD`.

- **A1, the wiring reverted** (the `flows: exportsOf(flows),` line
deleted, then `create-objectstack` rebuilt). `ablation-dist-preflight
--absent` confirmed the line was gone from `dist/`. Chain pin: 2 failed,
2 passed. The control and the scaffold stayed green, and `os g flow`
printed the wiring lines while validate read no `Logic: 1 Flows`. Parity
pin: 1 failed, 19 passed, on the stack-key comparison. Direction: red.
- **A1 restore.** Rebuilt; `ablation-dist-preflight` found the marker
present in `dist/templates/blank/objectstack.config.ts`, and the whole
tree was clean. Chain pin 4/4, parity pin 20/20.
- **A2, a barrel dropped from the template** (the `skills` key deleted):
parity 1 failed, 19 passed. Red.
- **A3, one barrel's bytes drifted from what `os init` writes**
(`views/index.ts` reworded): parity 1 failed, 19 passed. Red.
- After A2 and A3 the whole tree was clean, and parity was 20/20.

## Verification

Patch round 1, at HEAD `702a27775` (origin/main `a88a1bb39` merged at
`df0c0c846`): the 124 derived gates, `check-issue-citations --base
origin/main` and `check:scaffold-emission-policy` all exited 0 on the
first pass (`--ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN),
including `check:doc-anchors`, `check:docs-audit-scope` and
`check-affected-docs`; `pnpm lint` exited 0; the parity pin 20/20, the
chain pin 4/4, and `create-objectstack` 16 files, 232 passed.

Round 0: all of the following ran at HEAD `d50d46fe0` (origin/main
`26daf0b03` merged).

- `pnpm --filter create-objectstack test`: 16 files, 232 passed.
`typecheck`: exit 0.
- `pnpm --filter @objectstack/cli typecheck`: exit 0, including
`check:test-typecheck`, whose ledger is unchanged.
- CLI `unit` project: 231 files, 3316 passed.
- CLI `integration`: this chain pin plus `generate-stack-reach.test.ts`,
2 files, 11 passed.
- `pnpm lint`: exit 0 over the whole repo, not narrowed.
- `node scripts/check-issue-citations.mjs --base origin/main`: exit 0.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN. Three gates first
exited 3 with PREREQUISITE NOT MET (`check:skill-examples`,
`check:dual-build-cjs-loads`, `check:i18n-coverage`) and exited 0 after
a full build.
- `pnpm check:scaffold-emission-policy`: exit 0.

## Docs this change made false, and a surface note

These published lines described an objects-only starter and are
corrected in place:

- the blank starter's `README.md` Layout, plus its app remedy, which now
says to export the file from `src/apps/index.ts`;
- the shipped `AGENTS.md` rule 3, which prescribed `Object.values()`
(measured TS2322 on the now-empty barrels);
- the package `README.md` tree;
- `content/docs/getting-started/your-first-project.mdx`: its section-2
tree and config block;
- `content/docs/getting-started/build-with-claude-code.mdx` (patch round
1): step 3 said the agent wires the action, view and app through
`actions:` / `views:` / `apps:` keys in `defineStack()`. It now says
each file is exported from its directory's barrel
(`src/actions/index.ts`, `src/views/index.ts`, `src/apps/index.ts`),
which the starter's config already hands to `defineStack()`, matching
the shipped `AGENTS.md` rule 3. A sweep of `content/docs/` found no
other sentence telling a starter author to add a collection key;
- `content/docs/deployment/cli.mdx`.

In `cli.mdx`, the `os generate` section's "Not wired" example named "the
`npm create objectstack` starter", and its first-app walkthrough ran `os
generate action approve`. On the wired starter that action is refused
with exit 1: "Action 'approve' references object 'my_app_approve' which
is not defined in objects". The walkthrough now runs `object customer`,
then `flow customer`, then `action customer`, measured `UI: 1 Actions`
and `Logic: 1 Flows`. Its fixture callout now names the extra action.

`content/docs/**` and `packages/create-objectstack/README.md` were
outside the claim's first file surface; the seat amended the claim in
place to name them. They are edited under the agent contract's rule that
a published line this change makes false is repaired in the same PR. PR
objectstack-ai#20341 edits `cli.mdx` around lines 1619 to 1690, disjoint from these
hunks; PR objectstack-ai#20258 edited lines 1 to 7 of `your-first-project.mdx` and
`build-with-claude-code.mdx`, has since landed, and merged into this
branch without conflict.

`skills/objectstack-platform/SKILL.md` line 192 says the template
declares `requires: ['automation']`. That is now stale, but `skills/**`
is a governed Tier H surface, so it is **not** edited here; the seat
files it for the skills lane once this PR lands.

## Acceptance notes

- Byte-identical barrels inherit the article slip in `init.ts`'s
`renderEmptyWiredBarrel` ("a action", "a app"). `init.ts` is read-only
here. Whoever next edits that renderer carries it, and the parity pin
will then require the starter to follow.
- The old walkthrough's `os generate flow onboarding` also bound its
flow to an undeclared object. That was not silent: `os dev` warned "the
flow will never fire". The new walkthrough binds to the object it
creates.
- Measured in patch round 1, on a scaffolded starter holding the Build
with Claude Code step-3 files: exporting each from its barrel, with the
config untouched, gives `os validate` exit 0 with `Data: 2 Objects 6
Fields` and `UI: 1 Apps 1 Views 1 Actions`, the page's step-4 counts.
Adding `actions:` / `views:` / `apps:` keys beside the wired ones
instead still validates (the later key wins), but the starter's `tsc
--noEmit` fails with 3 x TS1117, and a later `os g view customer` then
reports Not wired, while the barrel-wired project reports it reaches the
stack.
- The `requires` pair is PR objectstack-ai#20329's shape. The standing family cards
for the rest of that seam are objectstack-ai#20331 and objectstack-ai#20332, both named on objectstack-ai#20215.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… without automation (objectstack-ai#20332) (objectstack-ai#20365)

Fixes objectstack-ai#20332

Clause-②: no (narrowing)

**BREAKING** (`@objectstack/spec` `minor`): a published accept set
narrows. No export, no error code and no accepted shape is added. The
refusal reuses `STACK_TRIGGER_CAPABILITY_REQUIRED` (`status: 422`).

## What changes

`validateTriggerCapability` (`packages/spec/src/stack.zod.ts`) refused
an auto-launched flow only when `requires` lacked `'triggers'`. Its
docblock said the trigger "is installed by ONE token, `requires:
['triggers']`". That is false. Every trigger plugin installs its trigger
into the automation service at `kernel:ready`, and without that service
it warns and installs nothing. No runtime's resolver turns `triggers`
into `automation`. So a stack with `requires: ['triggers']` and a
`record_change` flow passed `os validate`, booted, and never fired the
flow.

Triage direction `5861188312`, verbatim: 「**The contract choice, decided
here: refuse, do not imply.**」

The refusal now has three arms, one line per offending flow, on the same
code, header and `issues` shape. Each prescription is the whole fix for
the `requires` it was given:

| `requires` (auto-launched flow present) | Before | After |
|:---|:---|:---|
| `['automation', 'triggers']` | accepted | accepted (any order) |
| `['automation']` | refused: add `'triggers'` | **unchanged, byte for
byte** |
| `['triggers']` | **accepted** (the defect) | refused: add
`'automation'` |
| `[]` or absent | refused: add `['triggers']` | refused: add
`['automation', 'triggers']` |
| any, with no auto-launched flow, or only `obsolete` / `invalid` flows
| accepted | accepted |

### The refusal text, quoted (for review)

New arm, `requires: ['triggers']`:

```text
flow 'task_fanout' declares a 'record_change' trigger but `requires` does not include 'automation' — 'triggers' installs the 'record_change' trigger into the automation service, and without it no 'record_change' trigger would be registered, so the flow would never auto-launch. Add 'automation' to requires: ['automation', 'triggers'] (@objectstack/service-automation runs the flow; @objectstack/trigger-* only fires it).
```

Changed arm, neither token:

```text
flow 'task_fanout' declares a 'record_change' trigger but `requires` does not include 'automation' or 'triggers' — no 'record_change' trigger would be registered, so the flow would never auto-launch. Add requires: ['automation', 'triggers'] (record_change/schedule/time_relative/api ship in @objectstack/trigger-* and install into @objectstack/service-automation — 'triggers' alone installs nothing).
```

Unchanged arm, `requires: ['automation']`:

```text
flow 'task_fanout' declares a 'record_change' trigger but `requires` does not include 'triggers' — no 'record_change' trigger would be registered, so the flow would never auto-launch. Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in @objectstack/trigger-*).
```

### A choice made here: the neither-token message names both tokens

The dispatch left this open and suggested keeping today's message.
Analysed on the four axes:

- **Real business need (measured).** Every real producer that declares
an auto-launched flow declares both tokens: `examples/app-showcase`,
`examples/app-todo`, the `os init` template, and `os g flow`'s own
output. The CLI boot banner already prescribes `requires: ['automation',
'triggers']`. No producer uses `['triggers']` alone.
- **Long-term soundness.** One prescription that satisfies the contract.
The message states the real install fact, the pair.
- **Keeping AI authors from writing it wrong.** Keeping the old message
would make an author who follows it literally land on `['triggers']`,
and the new arm would refuse them a second time. A prescription that
leads to another refusal is a trap. The pin `each prescription, applied
literally once, is ACCEPTED` holds the property.
- **No scope growth.** No new code path, code or export. Only the text
of one arm changes. No pin anywhere parsed the old text for this case.

## Measurements (dispatch Zone 2)

**§1 Reproduce on both runtimes.**

- **Framework CLI**, at this branch with the pre-fix early exit (`if
(hasTriggers) return errors;`) built into `packages/spec/dist`. The
mutation was held by `scripts/ablation-replace.mjs` and confirmed in
`dist/` by `scripts/ablation-dist-preflight.mjs`. On a probe project
with `requires: ['triggers']` and one `record_change` flow:
  - `os validate` exited **0**.
- `os serve --dev` reached `Server is ready` and printed `⚠ Flows: 1
flow(s) declared but the automation engine is not enabled — they will
never run. Add requires: ['automation', 'triggers'] to
objectstack.config.ts`.
- It also printed four warns: `RecordChangeTriggerPlugin` /
`ScheduleTriggerPlugin` / `TimeRelativeTriggerPlugin` /
`ApiTriggerPlugin: automation service not available — … trigger NOT
installed`.
- After the restore leg (source equal to HEAD, spec rebuilt, marker
absent from all 222 dist files), `os validate` on the same probe exits
**1** with the new message.
- **cloud** `origin/main` `96eb092`, read only:
- `packages/objectos-runtime/src/capability-loader.ts`
`resolveCapabilityDependencies` pulls `queue`, `job` and `messaging` for
`triggers`, never `automation`. So the resolver does not imply it.
- The hosted hosts force-mount both tokens on every tenant environment,
whatever the artifact declares: `apps/objectos/hosted-slate.ts`
`HOSTED_FORCED_REQUIRES`, and `apps/objectos-ee` `defaultRequires`.
There a `['triggers']` stack runs, but so does a stack with no
`requires` at all. The published arm has refused that second stack since
it landed. The refusal judges the stack's own declaration, which is
portable, and not one host's floor.
- The fork condition ("`triggers` pulls `automation` in by itself")
holds on neither resolver.

**§2 Which kinds need `automation`.** All four. The boot above printed
all four "NOT installed" warns. Each plugin's `kernel:ready` hook
resolves `automation` and returns if it is missing:
`packages/triggers/trigger-record-change/src/plugin.ts:47`,
`trigger-schedule/src/plugin.ts:72`,
`trigger-schedule/src/time-relative-plugin.ts:67`,
`trigger-api/src/plugin.ts:62`. No kind stays accepted.

**§3 Shape and door.**

- One arm beside the existing one, same class and code.
- `os validate` reaches it through `loadConfig`, which evaluates the
author's `defineStack()` call (step 1), and not through its own stack
parse (step 2). It is not a separate door.
- Measured through the real CLI (`tsx bin/run-dev.js validate`) at this
branch: `['triggers']` exits 1, `[]` exits 1, `['automation',
'triggers']` exits 0.

**§4 Producer census** (expected 0 producers; 0 found; three test
stacks):

- `examples/**`:
- `app-showcase` and `app-todo` declare both. `os validate` exits 0 on
each at this branch.
- `app-crm` declares `['ui', 'automation']` and has no auto-launched
flow; `os validate` exits 0.
  - `app-multi-package` and `embed-objectql` declare no `requires`.
- `packages/create-objectstack/src/templates/**`: `blank` declares
`['automation']`. It is out of this arm's reach.
- `os init` / `os g` on current `main` (PR objectstack-ai#20329 landed as
`c5dcb3ba07`): `init` declares both. `os g flow` into a `['triggers']`
project was the "cannot run" answer and is now a refusal (see below).
- `packages/**` test stacks with `triggers` alone and an auto-launched
flow:
- `packages/cli/test/generate-object-namespace-prefix.test.ts`:
converted to the pair.
- `packages/qa/dogfood/test/fixtures/override-composite-fixture.ts`:
converted. Its boot mounts both explicitly; `verify`'s harness does not
read `requires`.
- `packages/lint/src/authoring-rule-input-tier.test.ts:93`: **not
converted.** `packages/lint` is fenced read-only for this dispatch. See
"Owed, and fenced".
- cloud `main`: six `requires` literals with `triggers` and no
`automation`, all loader and publish-route token-list tests. No
`defineStack`, no flows, so none is affected.
- hotcrm: NOT MEASURED. It is not a repository in this container.

**§5 The one-token sentence**, restated at every site that repeated it:

- the `validateTriggerCapability` and
`StackTriggerCapabilityRequiredError` docblocks;
- `automation/flow-trigger-kind.ts`;
- the error-code ledger comment;
- the two spec test comments;
- `content/docs/automation/flows.mdx` and
`content/docs/permissions/capabilities.mdx`.

`@objectstack/lint` carries no trigger-capability rule of its own; it
shares only `resolveFlowTriggerKind`. There is no asymmetry to report.

## Pins (table-driven: requires × kind × status)

`packages/spec/src/stack-requires.test.ts`, new block. Each refusal is
asserted as its envelope (`code`, `status: 422`, one `issues` entry per
flow) plus the prescription text:

- `['triggers']` refused for `record_change`, `schedule`,
`time_relative` and `api`;
- `['automation', 'triggers']` accepted for all four, and in any order
(the control);
- `[]` and absent: the pair named, and `Add requires: ['triggers']`
asserted absent;
- `['automation']`: today's `issues` entry asserted with `toEqual`;
- no auto-launched flow (none, a `screen` flow, a hand-launched
`autolaunched` flow) unaffected;
- `obsolete` / `invalid` unaffected, while `draft` / `active` are
refused;
- four flows give four issues;
- each prescription, applied once, is accepted.

**Ablation**, committed first and restored by blob hash:

- **Leg A:** the old early exit. `7 failed | 23 passed`.
- **Leg B:** the neither case given the old message. `3 failed | 27
passed`.
- **Restored:** `30 passed`, blob `4c0bfced0f02` equal to HEAD.
- Direction as predicted (turns red).

**`os g` pin flipped**
(`packages/cli/test/generate-stack-reach.test.ts`, landed with PR
objectstack-ai#20329). `os g flow order_line` into a `['triggers']` project was pinned
as "cannot run", exit 0. The written flow now stops the config from
loading, so the write is refused. The pin is now: exit 1, the project
tree byte-identical, the flow file absent, stdout names `does not
include 'automation'` and prints `requires: ['automation', 'triggers']`,
and no `Created`. Result: `7 passed`.

## Tests and gates

The branch head is `94e32023d4`: a merge of `origin/main` `6704717188`
made through `scripts/pm/os-regen-merge.sh`. It touched
`error-code-ledger.zod.ts` on both sides, and both edits survive. Every
reading below was taken at that head unless it names `45cfbaafee`, the
last commit before the merge. The merge brought no change to any file
those older readings depend on.

- `@objectstack/spec` at `94e32023d4`:
  - `vitest run --project local`: **554 files, 16439 passed, 1 todo**.
  - `typecheck` exit 0.
  - `check:generated`: every artifact up to date, after a rebuild.
- `@objectstack/cli` at `45cfbaafee`:
- `generate-object-namespace-prefix` and `generate-scaffold-wiring`
(unit): **52 passed**.
  - `generate-stack-reach` (integration): **7 passed**.
- `@objectstack/lint`, the consumer suite, at `45cfbaafee`: **1 failed |
4303 passed**. The one failure is the fenced fixture described below.
Its new message, re-read at `94e32023d4`, is the refusal it should be.
This is expected, and owed.
- dogfood: the fixture module loads (`requires =
["automation","triggers"]`). The boot pin itself is left to CI's Dogfood
Regression Gate.
- `os validate` on the examples at `45cfbaafee`: `app-todo`,
`app-showcase` and `app-crm` each exit 0.
- Gates: `dispatch-gates --commands` at `94e32023d4` derives 112. All
112 ran, each with its exit code recorded, and `--ran` reconciles them:
**112 run, 0 NOT-MEASURED, 0 UNRUN**.
- Exit 0: 111. This includes every `@objectstack/spec` `check:*` in the
list (`api-surface`, `authorable-surface`, `docs`,
`error-code-provenance`, `liveness`, `skill-examples`),
`check:type-check-debt`, `check:dual-build-cjs-loads`, and the root
`check:*` and `node scripts/*` set.
- Exit 1, red by design: `check-empty-changeset`. See the next section.

## Deliberate correction of a pending release note

`.changeset/20215-generate-scaffolds-reach-stack.md` (from PR objectstack-ai#20329,
unreleased) says two things this PR makes false:

- "Without `automation`, the server loads the flow and never runs it."
- that `os g` reports **cannot run** for a flow in a stack whose
`requires` lacks `automation`.

Both sentences are corrected in place. `content/docs/deployment/cli.mdx`
gets the same correction. `check-empty-changeset` stays red on this, by
design ("DELIBERATE CORRECTION … say so on the PR and get it
confirmed"). **This needs a person's confirmation.** Restoring the file
from base would put the false sentences back into the next release.

## Owed, and fenced (not in this PR)

These are outside the dispatch's fence, so this PR does not touch them:

1. `packages/lint/src/authoring-rule-input-tier.test.ts:93`: `requires:
['triggers']` becomes `['automation', 'triggers']`, and its comment
names the pair. Until then `@objectstack/lint`'s suite is red on that
one test.
2. `packages/cli/src/commands/init.ts` (the emitted config comment,
`:649–652`, and the `SCAFFOLD_WIRED_REQUIRES` docblock) and
`packages/cli/src/commands/generate.ts` (the emitted flow-file header,
`:366–369`, and its docblock). Each says that without `automation` "the
server loads the flow and never runs it". After this change the config
stops loading.

## Acceptance notes

- The `cannot run` branch of `os g`'s reach report has no scaffold that
reaches it now. The flow scaffold's only tokens are the pair, and a
stack missing either is refused. This is dead for today's generators. It
is noted and not filed. Carrier: the next PR to touch
`packages/cli/src/commands/generate.ts`.
- `os validate` on a config that exports a plain object instead of
calling `defineStack()` skips every `defineStack` cross-field refusal,
this one included. Measured: a plain-object probe with `requires:
['triggers']` and a `record_change` flow exits 0 at this head. This is
the whole refusal family's door, not this arm's, so it is reported to
the seat in the dev report and not filed here.

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

---------

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/xl tests tooling

Projects

None yet

2 participants