Skip to content

feat(cli): loop-first agent context, universal requirements pages, init --dialect - #557

Merged
dmealing merged 4 commits into
mainfrom
fm/mo-onboarding-fixes
Nov 12, 2026
Merged

dmealing merged 4 commits into
mainfrom
fm/mo-onboarding-fixes

Conversation

@dmealing

@dmealing dmealing commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Rewrites the always-on agent context template (agent-context/templates/always-on.md.mustache) loop-first, with per-stack loop fragments under agent-context/templates/loops/ (TypeScript, C#, Java, Kotlin, Python, client). The long principles, adoption, JVM, port-sync and upgrade text moves into the metaobjects-codegen and metaobjects-verify skills. Migration docs gain --rename-column, --rename-table and --allow. Mid-task skill-trigger evals are added under agent-context/evals/. The always-on file no longer tells agents to cat SKILL.md files.
  • meta init installs the requirements reference pages in every stack. The concern-gating machinery (CONCERN_TOKENS, detectConcerns, hasRequirementNodes, Stack.concerns) is removed. meta gen and meta verify print an advisory when the model declares requirements and the scaffolded context has no requirements pages. meta gen also advises when no requirement-tests generator is wired (naming requirementTests() and meta eject requirement-tests). Exit codes are unchanged.
  • meta init --dialect sqlite|postgres|mysql|d1 sets the codegen dialect, and migrate.dialect for sqlite, postgres and d1. --d1 equals --dialect d1, and a conflicting pair is refused. MySQL is codegen-only. Next steps are dialect-aware, and the scaffolded config comment is shorter.

Not included: a compact meta gen --list for non-TTY output. The off-TTY default stays TOON, a structured format that tests and skills parse. The always-on file documents a one-line grep/paste filter instead.

Validation: two no-mistakes runs. The first passed with one review note: the new "Renames and destructive changes" section in migration.md split its numbered workflow. The second run moved that section after the list and passed with no findings. A full scripts/ci-local.sh --quick and the sdk and cli suites also passed locally before the first run.

Also: content hashes, the agent-context conformance fixtures (the ts-requirements case is replaced with a csharp case), and CHANGELOG entries under Unreleased. Versions and the release header are unchanged.

Validation

Risk Assessment

✅ Low: Changes are agent-context docs and templates, a bounded --dialect flag and two advisories that never touch exit codes; the concrete generator class names, flags and allow tokens the new text cites exist in source.

Testing

I drove the real meta CLI from source in disposable projects. All init dialect cases behave as specified (config values, conflict and bogus-dialect refusals at exit 2). The meta gen advisory for requirements with no requirement-tests generator fires and the exit code stays 0. The 'agent context has no requirements pages' advisory fires on gen and verify, and meta init --docs-only --refresh-docs clears it. A model with no requirements gets neither advisory. The scaffolded always-on file is about 4 KB, loop-first, documents --rename-column, --rename-table and --allow, and has no instruction to read SKILL.md files. migration.md now has "Renames and destructive changes" after step 3 and before "Dialects", and the documented flags match meta migrate --help. Regression: baseline ci-local (ts-fast, ts-unit) was already green, and I ran the sdk, metadata, cli and codegen-ts suites separately with 0 failures. A single run of the whole server suite exceeded the 10-minute background limit, so I ran it per package; the Java, C#, Python and Kotlin lanes were not run. The "no advisory once requirementTests() is wired" case is covered only by its unit test, since wiring the import in a disposable project was not driven live. This is CLI text output with no UI surface, so there are no screenshots; the transcripts are saved as evidence files.

  • Live validation: ✅ go - 9 of 9 scenarios driven live against the product
Scenario Result Live Evidence
meta init --dialect sqlite|postgres|mysql|d1 writes the right codegen and migrate dialect, with dialect-aware next steps (CLI, throwaway projects) ✅ pass live init-dialects.txt, init-postgres.txt: postgres and sqlite write migrate.dialect and the matching step 5; d1 writes codegen sqlite and migrate d1; mysql writes codegen mysql, no migrate block, and says…
Conflicting or invalid dialect is refused (--d1 --dialect postgres, --dialect oracle); --d1 --dialect d1 accepted and equals --d1 (CLI) ✅ pass live init-dialects.txt: conflict and bogus exit 2 with a clear message and nothing scaffolded; d1+d1 exit 0; --d1 gives migrate.dialect d1
Scaffolded metaobjects.config.ts comment is short, carries the generator import line and has no literal generators: [ in the comment ✅ pass live init-postgres.txt / scaffolded file printed: the only generators: [] is the real list at the end
meta init installs requirements pages in every stack, so a fresh project has the three requirements.md pages ✅ pass live init in a project with no declared server: .claude/skills/metaobjects-{audit,authoring,verify}/references/requirements.md present
meta gen advisory when requirements exist and no requirement-tests generator is wired, naming requirementTests() and meta eject requirement-tests, exit code unchanged ✅ pass live advisories.txt: warnings entry printed, exit 0
meta gen and meta verify advisory when requirements exist and the scaffolded context lacks the pages; meta init --docs-only --refresh-docs fixes it; no advisory when the model has no requirement… ✅ pass live advisories-missing-pages.txt: advisory on both commands with exit 0, 3 pages reinstalled by refresh, advisory count 0 afterwards; alwayson.txt: no-requirements model produced 0 advisories on gen and v…
Always-on file is loop-first and about 5 KB, documents rename flags and --allow, has a 'Going deeper' list saying the loop needs no skill, and tells agents nothing about reading SKILL.md ✅ pass live alwayson.txt: 4030-byte AGENTS.md identical to CLAUDE.md; skill-read grep empty; --rename-column, --rename-table, --allow drop-check/drop-column present and match migrate --help
migration.md 'Renames and destructive changes' no longer splits the numbered workflow (run-2 follow-up) ✅ pass live alwayson.txt heading outline: step 3 Apply at line 117, Renames at 129, Dialects at 162
Regression: sdk, metadata, cli and codegen-ts test suites ✅ pass live pkg-suites.log: sdk 437, metadata 3965, cli 2006, codegen-ts 3444 pass; 0 fail
Evidence: init --dialect transcripts (all dialects, conflicts)
=== --dialect sqlite
exit 0
20:  5. Create your tables: meta migrate --from-db --db file:dev.sqlite --dialect sqlite --slug init --apply
22:       meta migrate apply-pending --db file:other.sqlite
5:  dialect: "sqlite",
migrate: {"dialect":"sqlite"}
=== --dialect mysql
exit 0
20:  5. Schema: meta migrate does not own MySQL schema, so create and change the tables with
5:  dialect: "mysql",
migrate: undefined
=== --dialect d1
exit 0
20:  5. Create your tables: meta migrate --dialect d1 --slug init writes migrations/<seq>_init.sql;
5:  dialect: "sqlite",
migrate: {"dialect":"d1"}
Evidence: init --dialect postgres full output
$ meta init --dialect postgres

Initialized metaobjects/ + .metaobjects/ + metaobjects.config.ts
codegen/generators/ is EMPTY on purpose: no code is generated until you choose it.

Next steps:
  0. Everything here is ESM — package.json needs "type": "module" (init sets it
     unless the project has CommonJS sources; without it the first tsc fails).
  1. Author entities under metaobjects/ (start from the scaffolded meta.common.json)
  2. meta gen --list --probe    # the catalog: every generator, grouped by layer, with
                                #   how many files each would emit for YOUR model
  3. Wire the ones you want in metaobjects.config.ts: each catalog row prints the
     import that runs it as shipped.
     meta eject <name>...       # need to change what one emits? copies it into
                                #   codegen/generators/ (yours to edit) and prints the
                                #   import to swap in and what to install
     meta generator new <name>  # an output no reference emits? write your own — this
                                #   scaffolds a working one and wires it
  4. meta gen                  # generate from exactly what you wired, documentation
                                #   included (the docs layer of the catalog)
  5. Create your tables (an empty, reachable database):
       meta migrate --from-db --db postgres://<user>@<host>/<db> --dialect postgres --slug init --apply
     A new database later (a fresh clone, CI, tests) replays the committed migrations:
       meta migrate apply-pending --db postgres://<user>@<host>/<other-db>
Evidence: requirement-tests advisory and verify transcripts
\### 1. gen, pages present, no requirement-tests wired
gen: []
summary: "no generators are wired in metaobjects.config.ts, so nothing was generated"
help[1]: "add generators to the `generators: []` array in metaobjects.config.ts — `meta gen --list` names each one with the import that runs it as shipped, and `meta eject <name>` copies one into your repo to change what it emits"
antiPatterns:
  status: ran
  total: 0
  rows: []
warnings[1]: "The model declares 1 requirement and no generator writes their tests. Wire `requirementTests()` (import it from \"@metaobjectsdev/codegen-ts/generators\") for one test stub per requirement, or `meta eject requirement-tests` to own the template."
exit 0
\### 2. verify, pages present
requirementLedger[1]:
  - path: placeOrder
    package: "acme::shop"
    qualifiedPath: "acme::shop::placeOrder"
    subType: functional
    level: 4
    status: planned
    digest: 0e66a3a24cdf65a5f3f8758c2b6b9a5aca3889dfa54af390ebe0af3688f3c1c2
    implementedBy: []
    diagnostics: []
    verdict: clean
notRepresented[2]: "per-gate drift detail for every gate but schema (which template variable drifted, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict, and the schema gate's differences in schemaDrift",the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr
exit 0
\### 3. delete requirements pages
gen: []
summary: "no generators are wired in metaobjects.config.ts, so nothing was generated"
help[1]: "add generators to the `generators: []` array in metaobjects.config.ts — `meta gen --list` names each one with the import that runs it as shipped, and `meta eject <name>` copies one into your repo to change what it emits"
antiPatterns:
  status: ran
  total: 0
  rows: []
warnings[1]: "The model declares 1 requirement and no generator writes their tests. Wire `requirementTests()` (import it from \"@metaobjectsdev/codegen-ts/generators\") for one test stub per requirement, or `meta eject requirement-tests` to own the template."
exit 0
exit 0
\### 4. refresh
Scaffolded the MetaObjects agent context (42 files): .metaobjects/AGENTS.md + .claude/skills/metaobjects-*.
Re-run --docs-only --refresh-docs to update; --no-wire-root to skip the root CLAUDE.md @import.
.claude/skills/metaobjects-audit/references/requirements.md
.claude/skills/metaobjects-authoring/references/requirements.md
.claude/skills/metaobjects-verify/references/requirements.md
0
Evidence: missing-requirements-pages advisory and refresh transcript
\### 5. simulate pre-change scaffold: drop requirements pages from disk AND manifest
-- gen
meta: The model declares requirement.* nodes, but the MetaObjects agent context was scaffolded without its requirements pages (when to set a requirement's @status, the requirement-tests generator). Re-run 'meta init --docs-only --refresh-docs' to install them.
warnings[2]: "The model declares 1 requirement and no generator writes their tests. Wire `requirementTests()` (import it from \"@metaobjectsdev/codegen-ts/generators\") for one test stub per requirement, or `meta eject requirement-tests` to own the template.","The model declares requirement.* nodes, but the MetaObjects agent context was scaffolded without its requirements pages (when to set a requirement's @status, the requirement-tests generator). Re-run 'meta init --docs-only --refresh-docs' to install them."
exit 0
-- verify
meta: The model declares requirement.* nodes, but the MetaObjects agent context was scaffolded without its requirements pages (when to set a requirement's @status, the requirement-tests generator). Re-run 'meta init --docs-only --refresh-docs' to install them.
exit 0
-- refresh
Scaffolded the MetaObjects agent context (42 files): .metaobjects/AGENTS.md + .claude/skills/metaobjects-*.
Re-run --docs-only --refresh-docs to update; --no-wire-root to skip the root CLAUDE.md @import.
3
-- gen after refresh (expect 0 matches)
0
-- verify after refresh (expect 0)
0
\### 6. no-requirements model: old scaffold, no advisory
Evidence: scaffolded always-on file and migration.md outline
\### no requirement model + old scaffold = no advisory
0
0
\### always-on file
# Working with MetaObjects in this project

> Stack: no server, no client.
>
> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
> **root `CLAUDE.md`**, not here — this file is regenerated.

The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of
truth. Code, schema and migrations are generated from it and are disposable.

## Start here: the whole loop, in order

No server language is declared for this project, so this file names no generate step. Declare it and refresh: `meta init --docs-only --refresh-docs --server <typescript|java|kotlin|csharp|python>`.


\### Schema (every stack: the Node `meta` CLI owns it)

Never hand-write DDL, and never `ALTER` a live database by hand.

- First migration (needs a reachable, empty database):
  `npx meta migrate --from-db --db <url> --dialect <dialect> --slug init` writes
  `.metaobjects/migrations/<timestamp>-init/up.sql` and `down.sql`.
- At boot or deploy: `npx meta migrate apply-pending --db <url>`.
- A later change: edit the metadata, regenerate, then
  `npx meta migrate --dialect <dialect> --slug <name>` (offline, from the committed snapshot).
  For a renamed field add `--rename-column <table>.<old_column>=<new_column>` (a renamed object:
  `--rename-table <old>=<new>`), which keeps the data. A destructive change is refused until you
  allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum,
  `--allow drop-column` only when the data should go.

\### Requirements, when the metadata declares `requirement.*` nodes

They list what the software must do. A commit that changes behaviour updates them in the same
commit: `@status: live` once a claim is built (never before), a new node for new behaviour.
`meta verify` checks that links resolve, not that a status is true.

\### Before you call it done

- `npx meta verify --db <url>` (the live schema matches the metadata).

Commit the metadata, the generated code, `.metaobjects/migrations/` and
`.metaobjects/.gen-state/.hashes.json`.

## Rules

- What the metadata describes is generated, never hand-written (tables, types, validators,
  foreign keys, schema, migrations). Hand-write only what it cannot describe, on top.
- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or
  broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on.
- Never spell a table or column name as a string; use the generated table or names constants.
- Wire a generator only for output you will import.
- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a
  template, the data in a payload, a structured reply read through its generated extractor
  (`metaobjects-prompts`).
- Never invent metadata vocabulary. Search it: `npx meta types <term>`, and
  `npx meta types <type>.<subType> --detail` for one construct's valid attributes.
- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are
  unique within a node, and package paths use `::`.

## Going deeper

The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work:
unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a
generator and the full principles (`metaobjects-codegen`), REST routes and the web client
(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and
upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when
its task comes up; it names the one reference page to open.

This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in
the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a
file you edited (only `--force` does, so never `--force` one with local edits): it writes
`<path>.new` beside it. Merge that into `<path>`, keeping your edits, and delete the `.new`.
\### skill-reading instruction?
\### migration.md
1:# Schema migrations — the shared TypeScript engine (every port)
17:## Contents
30:## Install (Node, dev-only)
46:## Output format
61:## The workflow
63:### Fresh database: create the tables with `--from-db … --apply`
93:### Generating a migration
95:1. **Generate a migration** by diffing metadata vs the prior state (the live DB or a
111:2. **Review the SQL.** Read the emitted `up.sql` (forward) and `down.sql`
117:3. **Apply** the pending migrations against the DB; migration history is tracked in
129:## Renames and destructive changes
162:## Dialects
170:## Live-DB drift: `meta verify --db`
183:## Index modeling (Postgres)
214:## Projection views (generated view DDL)
239:## DDL-ownership escape valves (`@sql` / `@unmanaged`) — #208
286:## Adopting an existing database (non-destructive)
308:## Not yet shipped
Evidence: migrate --help flags
  --allow <csv>        Comma-separated destructive-change permissions:
                       drop-column,drop-table,type-change,drop-index,drop-fk,
                       drop-check,drop-view,drop-view-cascade,drop-column-read-by-view,
                       drop-column DELETES the column and every value in it. If
                       the column was RENAMED, use --rename-column instead: it
  --rename-table [schema.]old=new
  --rename-column [schema.]table.old=new
Evidence: per-package suite results
sdk exit 0:  437 pass  0 fail 
metadata exit 0:  3965 pass  0 fail 
cli exit 0:  2006 pass  0 fail 
codegen-ts exit 0:  3444 pass  0 fail 

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 9 of 9 scenarios driven live against the product
Scenario Result Live Evidence
meta init --dialect sqlite|postgres|mysql|d1 writes the right codegen and migrate dialect, with dialect-aware next steps (CLI, throwaway projects) ✅ pass live init-dialects.txt, init-postgres.txt: postgres and sqlite write migrate.dialect and the matching step 5; d1 writes codegen sqlite and migrate d1; mysql writes codegen mysql, no migrate block, and says…
Conflicting or invalid dialect is refused (--d1 --dialect postgres, --dialect oracle); --d1 --dialect d1 accepted and equals --d1 (CLI) ✅ pass live init-dialects.txt: conflict and bogus exit 2 with a clear message and nothing scaffolded; d1+d1 exit 0; --d1 gives migrate.dialect d1
Scaffolded metaobjects.config.ts comment is short, carries the generator import line and has no literal generators: [ in the comment ✅ pass live init-postgres.txt / scaffolded file printed: the only generators: [] is the real list at the end
meta init installs requirements pages in every stack, so a fresh project has the three requirements.md pages ✅ pass live init in a project with no declared server: .claude/skills/metaobjects-{audit,authoring,verify}/references/requirements.md present
meta gen advisory when requirements exist and no requirement-tests generator is wired, naming requirementTests() and meta eject requirement-tests, exit code unchanged ✅ pass live advisories.txt: warnings entry printed, exit 0
meta gen and meta verify advisory when requirements exist and the scaffolded context lacks the pages; meta init --docs-only --refresh-docs fixes it; no advisory when the model has no requirement… ✅ pass live advisories-missing-pages.txt: advisory on both commands with exit 0, 3 pages reinstalled by refresh, advisory count 0 afterwards; alwayson.txt: no-requirements model produced 0 advisories on gen and v…
Always-on file is loop-first and about 5 KB, documents rename flags and --allow, has a 'Going deeper' list saying the loop needs no skill, and tells agents nothing about reading SKILL.md ✅ pass live alwayson.txt: 4030-byte AGENTS.md identical to CLAUDE.md; skill-read grep empty; --rename-column, --rename-table, --allow drop-check/drop-column present and match migrate --help
migration.md 'Renames and destructive changes' no longer splits the numbered workflow (run-2 follow-up) ✅ pass live alwayson.txt heading outline: step 3 Apply at line 117, Renames at 129, Dialects at 162
Regression: sdk, metadata, cli and codegen-ts test suites ✅ pass live pkg-suites.log: sdk 437, metadata 3965, cli 2006, codegen-ts 3444 pass; 0 fail
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • meta init --dialect postgres|sqlite|mysql|d1, --d1, --d1 --dialect d1, --d1 --dialect postgres and --dialect oracle, run from source with bun server/typescript/packages/cli/bin/meta.ts in throwaway /tmp projects; checked exit codes, metaobjects.config.ts, .metaobjects/config.json migrate, and the printed step 5
  • meta gen and meta verify in a project with one requirement.functional and the scaffolded context: the requirement-tests advisory, exit code 0
  • Old-scaffold simulation (requirements pages removed from disk and manifest): meta gen and meta verify print the refresh advisory, meta init --docs-only --refresh-docs reinstalls the 3 requirements pages, and the advisory goes quiet
  • Model with no requirements on an old scaffold: no advisory from meta gen or meta verify
  • Read the scaffolded .metaobjects/AGENTS.md (4030 bytes) and grepped it and the skills for cat/read-SKILL.md instructions; checked migration.md step order; compared the documented flags with meta migrate --help
  • bun test in cli targeted files (87 pass), sdk agent-context and conformance files (96 pass)
  • bun test per package: sdk 437, metadata 3965, cli 2006, codegen-ts 3444; all pass, 0 fail
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…, init --dialect

The always-on file meta init scaffolds now opens with the whole loop for the declared
stack: generate (one step per declared server, plus the web client), schema (first
migration, apply-pending, rename and allow flags), requirements, and the verify commands.
It drops from about 19 KB to about 5 KB; the principles move verbatim to
metaobjects-codegen/references/principles.md and the port-sync and upgrade text to
metaobjects-verify/references/upgrading.md. It no longer tells agents to read SKILL.md by
path.

- Requirements reference pages install in every stack (the concern gate observed nodes at
  init time, before a new project had any). meta gen and meta verify say when a scaffolded
  context lacks them.
- meta gen advises wiring requirementTests() when requirements exist and nothing tests them.
- meta init --dialect sqlite|postgres|mysql|d1, dialect-aware next steps, and a shorter
  scaffolded config comment carrying the generator import line.
- migration.md documents --rename-column, --rename-table and --allow.
- Skill trigger evals gain mid-task cases.
- meta gen --list off a terminal is unchanged: its TOON default is parsed by tools and skills.
…kflow list

The section split the numbered workflow, leaving step 3 (Apply) detached after it.
Conformance fixtures regenerated.
@dmealing
dmealing force-pushed the fm/mo-onboarding-fixes branch from 79e3f41 to bcddd8a Compare November 12, 2026 03:24
@dmealing
dmealing merged commit a4c515e into main Nov 12, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-onboarding-fixes branch November 12, 2026 04:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant