@ultimat3/cli 1.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CLAUDE.md +761 -0
  2. package/README.md +42 -9
  3. package/package.json +25 -23
  4. package/src/api-routes.ts +16 -0
  5. package/src/app-auth.ts +32 -0
  6. package/src/app-entities.ts +18 -0
  7. package/src/app-env.ts +103 -0
  8. package/src/app-load.ts +20 -3
  9. package/src/bin.ts +4 -3
  10. package/src/budgets.ts +134 -9
  11. package/src/cmd-build.ts +69 -21
  12. package/src/cmd-db-branch.ts +219 -0
  13. package/src/cmd-db.ts +458 -153
  14. package/src/cmd-deploy.ts +59 -6
  15. package/src/cmd-dev.ts +92 -18
  16. package/src/cmd-docs.ts +167 -0
  17. package/src/cmd-doctor.ts +74 -10
  18. package/src/cmd-env.ts +95 -0
  19. package/src/cmd-errors.ts +33 -13
  20. package/src/cmd-fix.ts +5 -1
  21. package/src/cmd-generate.ts +146 -111
  22. package/src/cmd-help.ts +16 -5
  23. package/src/cmd-i18n.ts +2 -0
  24. package/src/cmd-jobs.ts +47 -33
  25. package/src/cmd-mcp.ts +11 -2
  26. package/src/cmd-new.ts +14 -8
  27. package/src/cmd-planned.ts +55 -10
  28. package/src/cmd-policy.ts +1 -0
  29. package/src/cmd-registries.ts +3 -0
  30. package/src/cmd-secrets.ts +368 -0
  31. package/src/cmd-tasks.ts +1 -0
  32. package/src/cmd-test.ts +29 -24
  33. package/src/cmd-verify.ts +197 -25
  34. package/src/db-backfill.ts +401 -0
  35. package/src/db-branch.ts +269 -0
  36. package/src/db-destructive.ts +29 -0
  37. package/src/db-finding.ts +28 -0
  38. package/src/db-generate.ts +144 -0
  39. package/src/db-seed.ts +294 -0
  40. package/src/db-snapshot.ts +24 -0
  41. package/src/dev-assets.ts +108 -23
  42. package/src/dev-cache.ts +122 -0
  43. package/src/dev-dashboard.ts +19 -4
  44. package/src/dev-hooks.ts +27 -2
  45. package/src/dev-n-plus-one.ts +191 -0
  46. package/src/dev-queue.ts +105 -19
  47. package/src/dev-render.ts +158 -26
  48. package/src/dev-roles-fixture.ts +67 -0
  49. package/src/dev-roles.ts +167 -78
  50. package/src/dev-runtime.ts +117 -40
  51. package/src/dev-services.ts +15 -0
  52. package/src/dev-storage.ts +247 -0
  53. package/src/dev-sync.ts +107 -0
  54. package/src/dev-traces.ts +37 -7
  55. package/src/dispatch.ts +4 -2
  56. package/src/document-styles.ts +54 -0
  57. package/src/drift.ts +78 -10
  58. package/src/error-catalog.ts +8 -18
  59. package/src/error-codes.ts +192 -0
  60. package/src/error-contract.ts +29 -7
  61. package/src/error-fixes.ts +114 -0
  62. package/src/errors.ts +201 -138
  63. package/src/exec.ts +42 -8
  64. package/src/fix-command.ts +268 -0
  65. package/src/flag-number.ts +67 -0
  66. package/src/framework-scope.ts +49 -0
  67. package/src/generate-kinds.ts +97 -0
  68. package/src/guards.ts +186 -0
  69. package/src/index.ts +92 -15
  70. package/src/island-bundle.ts +166 -0
  71. package/src/island-routes.ts +50 -0
  72. package/src/jobs-driver.ts +33 -0
  73. package/src/jobs-json.ts +24 -0
  74. package/src/jobs-report.ts +17 -4
  75. package/src/mcp-db-target.ts +52 -27
  76. package/src/mcp-errors.ts +128 -19
  77. package/src/mcp-host.ts +44 -25
  78. package/src/messages.ts +93 -2
  79. package/src/metrics-endpoint.ts +64 -16
  80. package/src/migrations.ts +37 -4
  81. package/src/otlp-export.ts +64 -0
  82. package/src/output.ts +46 -16
  83. package/src/parse.ts +41 -3
  84. package/src/policy-facts.ts +38 -6
  85. package/src/policy-fixture.ts +14 -7
  86. package/src/prerender.ts +111 -2
  87. package/src/registry.ts +21 -3
  88. package/src/runtime-overrides.ts +66 -0
  89. package/src/safe-url-label.ts +24 -0
  90. package/src/scaffold-fixture.ts +10 -0
  91. package/src/scaffold-typecheck.ts +16 -38
  92. package/src/serve.ts +185 -13
  93. package/src/shell-quote.ts +15 -0
  94. package/src/source-files.ts +4 -0
  95. package/src/statement-loop.ts +74 -0
  96. package/src/style-csp.ts +18 -0
  97. package/src/sync-authenticator.ts +59 -0
  98. package/src/templates/action.ts +15 -30
  99. package/src/templates/admin-page.ts +103 -0
  100. package/src/templates/admin.ts +11 -7
  101. package/src/templates/backfill.ts +212 -0
  102. package/src/templates/entity.ts +72 -31
  103. package/src/templates/guard.ts +143 -0
  104. package/src/templates/index.ts +12 -1
  105. package/src/templates/island.ts +67 -0
  106. package/src/templates/job.ts +53 -13
  107. package/src/templates/naming.ts +17 -1
  108. package/src/templates/policy.ts +35 -28
  109. package/src/templates/query.ts +24 -5
  110. package/src/templates/resource.ts +19 -11
  111. package/src/templates/route.ts +90 -15
  112. package/src/templates/scaffold-app.ts +142 -45
  113. package/src/templates/scaffold-claude-agents.ts +149 -0
  114. package/src/templates/scaffold-claude-commands.ts +221 -0
  115. package/src/templates/scaffold-claude.ts +134 -0
  116. package/src/templates/scaffold-container.ts +46 -2
  117. package/src/templates/scaffold-db-package.ts +91 -0
  118. package/src/templates/scaffold-docs.ts +24 -5
  119. package/src/templates/scaffold-domain-package.ts +90 -0
  120. package/src/templates/scaffold-env.ts +87 -0
  121. package/src/templates/scaffold-i18n.ts +4 -1
  122. package/src/templates/scaffold-mcp-package.ts +49 -0
  123. package/src/templates/scaffold-package-shape.ts +25 -4
  124. package/src/templates/scaffold-repo.ts +116 -257
  125. package/src/templates/scaffold-roles.ts +68 -0
  126. package/src/templates/scaffold-ui-package.ts +56 -0
  127. package/src/templates/slice-foundation.ts +88 -0
  128. package/src/templates/wrap.ts +95 -0
  129. package/src/test-counts.ts +35 -0
  130. package/src/test-select.ts +30 -15
  131. package/src/test-shards.ts +20 -11
  132. package/src/test-workers.ts +50 -0
  133. package/src/ts-scan.ts +284 -15
  134. package/src/tsconfig-references.ts +103 -0
  135. package/src/verify-floor.ts +133 -0
  136. package/src/verify-step.ts +19 -0
  137. package/src/verify-test-run.ts +72 -0
  138. package/src/verify-tests.ts +160 -71
  139. package/src/version-loader.ts +20 -3
  140. package/src/workspace-checks.ts +87 -16
  141. package/src/write-line.ts +34 -0
@@ -0,0 +1,221 @@
1
+ // The slash commands `x new` writes into `.claude/commands/`: the three workflows every audited
2
+ // repo re-invented by hand — build a feature, plan one, run the gate. Deliberately NOT one command
3
+ // per generator: `x g <kind>` already is that command, and a slash wrapper over a shipped CLI
4
+ // command is the second path axiom 1 forbids.
5
+
6
+ import type { GeneratedFile, NameSet } from './naming';
7
+
8
+ const feature = (app: NameSet): string => `---
9
+ description: Build or fix one thing in ${app.kebab} end to end — name the primitive, generate it, wire it inside the boundaries, gate it with \`x verify\`.
10
+ argument-hint: <what you want built or fixed, plain language>
11
+ allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, Skill
12
+ ---
13
+
14
+ # /feature
15
+
16
+ You are a senior engineer on **${app.kebab}**, an Ultimate app. Read \`AGENTS.md\` before designing
17
+ anything — it is the short form of every rule below, and it wins where the two disagree.
18
+
19
+ **Done means \`x verify\` green.** A passing unit test is not done. A working \`x dev\` is not done.
20
+ Report what you actually ran, never what you assume passed.
21
+
22
+ ## Request
23
+ $ARGUMENTS
24
+
25
+ **The prompt is the context.** Scope, autonomy, whether to commit: read it from the words. Stop for
26
+ a real blocker — a destructive migration, a request that needs a ninth primitive, a dependency you
27
+ cannot justify. You cannot ask a subagent's user anything, so decide and flag it, or stop and say
28
+ why.
29
+
30
+ ## 1. Name the primitive first
31
+
32
+ Eight, closed: \`entity\` · \`policy\` · \`action\` · \`mutator\` · \`query\` · \`job\` · \`route\` · \`task\`.
33
+
34
+ | The ask sounds like | It is |
35
+ |---|---|
36
+ | "store a …", "a … has fields" | \`entity\` |
37
+ | "only the owner may …" | \`policy\` |
38
+ | "when the user clicks submit …" | \`action\` (writes) / \`mutator\` (writes one entity) |
39
+ | "show me the list of …" | \`query\` |
40
+ | "send it afterwards", "retry until it works" | \`job\` |
41
+ | "a page at /…" | \`route\` |
42
+ | "every night at 3am" | \`task\` |
43
+
44
+ A request that fits none is not one feature — split it until every piece is one of the eight. There
45
+ is no ninth: a new capability is a **function that returns** one of these, never a new kind of thing.
46
+ Say which primitive and which slice out loud before you write a file. When the answer is not
47
+ obvious, hand the request to the \`shape\` subagent — that pass is the whole reason it exists.
48
+
49
+ ## 2. Generate it — do not hand-write it
50
+
51
+ \`\`\`sh
52
+ x g <kind> <name> --feature <slice> # x g --help is the only list of kinds
53
+ \`\`\`
54
+
55
+ The generator writes the source, its test and its i18n keys as one unit and registers it. A
56
+ hand-written primitive compiles and then goes missing from \`x routes\`, \`x actions\`, \`x queries\`,
57
+ \`x jobs\`, \`x tasks\`, \`x policy list\` and \`x.manifest.json\` — every surface that is supposed to
58
+ project it. Edit what the generator wrote; never reproduce it.
59
+
60
+ Schema changes are generated too: edit \`entity.ts\`, then \`x db gen "what changed"\`. Never hand-write
61
+ SQL into \`packages/db/migrations/\`.
62
+
63
+ ## 3. Wire it inside the boundaries
64
+
65
+ | Boundary | The rule | What breaks without it |
66
+ |---|---|---|
67
+ | \`apps/web/site/\` | 0kb JS, may not import \`apps/web/app/\` | the static path pays the app's bundle |
68
+ | \`apps/web/app/\` | authed, streaming, hydrated | — |
69
+ | \`apps/web/api/\` | actions only, \`route.ts\` | a second HTTP surface |
70
+ | \`apps/web/shared/\` | a leaf: imports nothing of yours | an import cycle across surfaces |
71
+ | \`repo.ts\` | the only file that touches the database | authz bypassed by a raw read |
72
+ | routes | call actions and queries, never a repo | policy skipped |
73
+
74
+ Route files: \`page.tsx\` under \`site/\`/\`app/\`, \`route.ts\` under \`api/\`. **The directory is the URL** —
75
+ the filename never is. One interactive control on a 0kb page is an island: \`x g island <name> --at <dir>\`.
76
+
77
+ ## 4. Gate it
78
+
79
+ \`\`\`sh
80
+ x verify # the gate. green = shippable
81
+ x verify --json # the same steps, machine-readable
82
+ \`\`\`
83
+
84
+ Red is instructions, not a verdict: **every finding carries an executable \`fix:\` — run it verbatim
85
+ before improvising**, and \`x errors explain <CODE>\` expands any code it names. Never narrow the gate
86
+ to make it pass: there is no \`--only\` and no \`--skip\`, on purpose, and disabling a lint rule or
87
+ loosening a compiler flag is the same move wearing a different hat.
88
+
89
+ While you iterate, narrow the *feedback*, not the gate:
90
+
91
+ | | Command |
92
+ |---|---|
93
+ | one test file | \`bun test <path>/<file>.test.ts\` |
94
+ | one test by name | \`bun test -t '<name>'\` |
95
+ | a whole type | \`x test unit\` · \`x test contract\` · \`x test e2e\` |
96
+ | lint the files you touched | \`bunx biome check --write <paths>\` |
97
+ | types, once, when otherwise done | \`bun run typecheck\` |
98
+ | a broken environment | \`x doctor\` |
99
+
100
+ ## 5. Hard rules
101
+
102
+ Never a bare \`throw new Error\` — subclass \`UltimateError\` with a stable code, a cause and a
103
+ runnable \`fix:\`. No \`any\`; use \`unknown\` and parse. Named exports only. \`import type\` for types.
104
+ Tests next to the source as \`<file>.test.ts\`, failure case first — a test that cannot fail is not a
105
+ test. One file, one job. Every user-facing string through \`t()\`. Semantic tokens, never a raw
106
+ colour. Every date formatted with an explicit IANA time zone. Money is integer minor units plus an
107
+ ISO code, never a float. Bun only.
108
+
109
+ ## Output
110
+
111
+ \`\`\`
112
+ Primitive: <which of the eight> Slice: <dir>
113
+ Generated: <the x g invocations you ran>
114
+ Changed: <files>
115
+ Gate: x verify ✓ | ✗ <failing steps>
116
+ Deferred: <what you did not do, and why> [never omit this line]
117
+ \`\`\`
118
+ `;
119
+
120
+ const planx = (app: NameSet): string => `---
121
+ description: Write a short, self-contained plan for ${app.kebab} to docs/plans/, for another agent to execute.
122
+ argument-hint: [what you want done]
123
+ allowed-tools: Read, Glob, Grep, Bash, Write, Agent
124
+ ---
125
+
126
+ # /planx
127
+
128
+ Plan only. No implementation, no edits outside the plan file.
129
+
130
+ ## Goal
131
+ $ARGUMENTS
132
+
133
+ ## Steps
134
+
135
+ 1. **Read the code before planning against it.** If a claim in the ask is already false in the tree,
136
+ say so under *Risks* with the \`file:line\` that disproves it, and plan what is actually true.
137
+ 2. **Resolve the path.** The date comes from Bun in a named zone, never from the host's ambient one —
138
+ \`date +%F\` gives a different answer on two machines at the same instant, and this app formats no
139
+ date without an explicit IANA time zone.
140
+
141
+ \`\`\`sh
142
+ bun -e "console.log(new Intl.DateTimeFormat('en-CA', { timeZone: 'Etc/UTC' }).format(new Date()))"
143
+ \`\`\`
144
+
145
+ Then \`docs/plans/<YYYY-MM-DD>-<slug>.md\`. Slug is kebab-case, five words maximum. One file — a
146
+ plan split across a directory is a plan nobody reads to the end.
147
+ 3. **Write it.** Sections, in this order:
148
+
149
+ \`\`\`markdown
150
+ # <Title>
151
+
152
+ ## Goal
153
+ One or two sentences: what, and why.
154
+
155
+ ## Primitive
156
+ Which of the eight (\`entity · policy · action · mutator · query · job · route · task\`) and which
157
+ slice it lives in. If it fits none, the design is wrong — say so here instead of planning a ninth.
158
+
159
+ ## Files to change
160
+ - \`path:line\` — what changes, and why.
161
+
162
+ ## Steps
163
+ 1. Ordered, concrete. Name the \`x g\` invocation where one applies. Point at code, do not paste it.
164
+
165
+ ## Tests
166
+ - What to add, next to the source as \`<file>.test.ts\`. Command to run it.
167
+
168
+ ## Done when
169
+ - Acceptance criteria, ending in \`x verify\` green.
170
+
171
+ ## Risks
172
+ - Anything the executor must decide, and every claim in the ask the code disproves.
173
+ \`\`\`
174
+
175
+ ## Rules
176
+
177
+ - Fragments over sentences. \`file:line\` refs over prose. Tables for anything with three or more rows.
178
+ - Reference-only: point at the code, never re-explain it.
179
+ - No checkboxes. The plan is a map, not a tracker.
180
+ - The plan must obey the app's own rules — one way to do each thing, generators over hand-written
181
+ files, imports that never cross a surface boundary, a stable error code with a runnable \`fix:\` for
182
+ every new failure, and \`x verify\` green as the last line of *Done when*.
183
+
184
+ ## Output
185
+
186
+ \`\`\`
187
+ ✓ docs/plans/<YYYY-MM-DD>-<slug>.md
188
+ Next: run it, or /feature it.
189
+ \`\`\`
190
+ `;
191
+
192
+ const verify = (): string => `---
193
+ description: Run the gate and fix everything it reports.
194
+ allowed-tools: Read, Write, Edit, Glob, Grep, Bash
195
+ ---
196
+
197
+ # /verify
198
+
199
+ Run \`x verify\`.
200
+
201
+ Green: say so and stop.
202
+
203
+ Red: fix every finding, then re-run until green. Each finding carries a stable code, a cause and an
204
+ executable \`fix:\` — **run the \`fix:\` verbatim before improvising**, and use \`x errors explain <CODE>\`
205
+ when the cause is not enough. \`x verify --json\` gives the same steps machine-readably; \`x doctor\`
206
+ covers the case where the environment, not the code, is what is broken.
207
+
208
+ Do not narrow the gate to make it pass. There is no \`--only\` and no \`--skip\`; disabling a lint rule,
209
+ loosening a compiler flag, or deleting an assertion is the same evasion. Fix the code.
210
+
211
+ Report one line per fix: the code, the file, what changed.
212
+ `;
213
+
214
+ /** The three workflows, in the order a new app meets them. */
215
+ export function claudeCommandFiles(app: NameSet): readonly GeneratedFile[] {
216
+ return [
217
+ { path: '.claude/commands/feature.md', contents: feature(app) },
218
+ { path: '.claude/commands/planx.md', contents: planx(app) },
219
+ { path: '.claude/commands/verify.md', contents: verify() },
220
+ ];
221
+ }
@@ -0,0 +1,134 @@
1
+ // The `.claude/` directory `x new` writes: the harness half of what an agent reads, next to the
2
+ // `AGENTS.md`/`CLAUDE.md` half that already shipped. Every file lands in the app's own repo, shows
3
+ // up in the scaffold's diff and is deletable in one line — the framework ships the mechanism, the
4
+ // app keeps or replaces the convention. Nothing here reaches outside the project directory.
5
+
6
+ import type { GeneratedFile, NameSet } from './naming';
7
+ import { claudeAgentFiles } from './scaffold-claude-agents';
8
+ import { claudeCommandFiles } from './scaffold-claude-commands';
9
+
10
+ /**
11
+ * Read-only commands and the two write commands whose blast radius is a file the gate checks.
12
+ * Deliberately absent: `x db reset`, `x db backfill --write`, `x secrets`, `x deploy` — each is
13
+ * destructive or reaches production, and an allowlist that covers them is a prompt nobody reads.
14
+ *
15
+ * The hook is the one part of the bundle that ACTS: `--write` rewrites the file just edited, in
16
+ * place. JSON takes no comment (the lesson `scaffold-repo.ts`'s `biome.json` already paid for), so
17
+ * the disclosure lives in `.claude/README.md` under "The hook" — and a test pins it there.
18
+ */
19
+ const settings = (): string => `{
20
+ "permissions": {
21
+ "allow": [
22
+ "Bash(bun install)",
23
+ "Bash(bun test:*)",
24
+ "Bash(bun run typecheck)",
25
+ "Bash(bun run lint)",
26
+ "Bash(bunx biome check:*)",
27
+ "Bash(x verify:*)",
28
+ "Bash(x test:*)",
29
+ "Bash(x doctor:*)",
30
+ "Bash(x g:*)",
31
+ "Bash(x routes:*)",
32
+ "Bash(x actions:*)",
33
+ "Bash(x queries:*)",
34
+ "Bash(x entities:*)",
35
+ "Bash(x jobs ls:*)",
36
+ "Bash(x jobs show:*)",
37
+ "Bash(x tasks:*)",
38
+ "Bash(x policy:*)",
39
+ "Bash(x i18n check:*)",
40
+ "Bash(x errors:*)",
41
+ "Bash(x env check:*)",
42
+ "Bash(x manifest:*)",
43
+ "Bash(x db gen:*)",
44
+ "Bash(x db migrate:*)",
45
+ "Bash(x db branch:*)",
46
+ "Bash(git status)",
47
+ "Bash(git diff:*)",
48
+ "Bash(git log:*)"
49
+ ]
50
+ },
51
+ "hooks": {
52
+ "PostToolUse": [
53
+ {
54
+ "matcher": "Edit|Write",
55
+ "hooks": [
56
+ {
57
+ "type": "command",
58
+ "command": "bunx biome check --no-errors-on-unmatched --write \\"$CLAUDE_FILE_PATHS\\" 2>/dev/null || true"
59
+ }
60
+ ]
61
+ }
62
+ ]
63
+ }
64
+ }
65
+ `;
66
+
67
+ const readme = (app: NameSet): string => `# .claude/
68
+
69
+ What Claude Code reads when it works on ${app.kebab}. Written by \`x new\`, owned by you from the
70
+ moment it lands: edit any file, or delete any file, and nothing in the app breaks. Nothing here is
71
+ enforced by \`x verify\`, and nothing here reaches outside this repository.
72
+
73
+ One thing in it does *write*: the \`PostToolUse\` hook in \`settings.json\` reformats each file an agent
74
+ edits, in place, the moment it is saved. That is the only thing here that touches your code on its
75
+ own — see [The hook](#the-hook), and delete the \`hooks\` block if you would rather format by hand.
76
+
77
+ | Path | What it costs | When you pay it |
78
+ |---|---|---|
79
+ | \`settings.json\` | nothing in context | read once per session; the hook runs per edit |
80
+ | \`commands/*.md\` | nothing until invoked | the file is read when you type \`/<name>\` |
81
+ | \`agents/*.md\` | one \`description\` line each | the body is read only when that agent is dispatched |
82
+
83
+ So the whole bundle is three description lines of standing cost. The rest is paid on use.
84
+
85
+ ## What is here
86
+
87
+ | File | For |
88
+ |---|---|
89
+ | \`commands/feature.md\` | \`/feature\` — build or fix one thing end to end, gated on \`x verify\` |
90
+ | \`commands/planx.md\` | \`/planx\` — write a plan to \`docs/plans/\` for another agent to execute |
91
+ | \`commands/verify.md\` | \`/verify\` — run the gate and fix what it reports |
92
+ | \`agents/shape.md\` | the idea-stage pass: which primitive, which slice, or "do not build this" |
93
+ | \`agents/data.md\` | \`packages/db/\`, \`entity.ts\`, \`repo.ts\` |
94
+ | \`agents/server.md\` | actions, mutators, queries, jobs, tasks, policies, \`apps/web/api/\` |
95
+ | \`agents/web.md\` | \`apps/web/site/\`, pages, \`packages/ui/\`, tokens, i18n |
96
+
97
+ The agents are scoped by **boundary**, not by role — each one's brief is a file set it may write and
98
+ a line it may not cross. That is what makes two of them safe to run at once.
99
+
100
+ ## What is deliberately not here
101
+
102
+ **No command that wraps \`x g\`.** \`x g <kind> <name>\` already is that command, and \`x g --help\` is
103
+ the only list of kinds — a slash command restating it is a second copy that drifts the first time a
104
+ kind is added.
105
+
106
+ **No size budget on this directory.** How much your app writes down is your convention, not the
107
+ framework's.
108
+
109
+ ## The hook
110
+
111
+ One \`PostToolUse\` hook: \`bunx biome check --write\` on the file that was just edited. **\`--write\`
112
+ means it rewrites that file in place** — the same safe fixes \`biome check --write .\` would make
113
+ repo-wide, but your working tree does change after every agent edit. Scoped to one file, so it costs
114
+ milliseconds, and it settles formatting arguments before they reach a diff.
115
+
116
+ It is not a typecheck, because a *scoped* one does not exist: \`tsc\` needs the project, and \`x verify\`
117
+ takes no \`--only\` and no \`--skip\` by design — narrowing the gate would make "green" mean whatever
118
+ the caller chose. If you want types on every edit, \`bun run typecheck\` is the whole project and you
119
+ are choosing to pay for it.
120
+
121
+ ## Where the rules live
122
+
123
+ \`AGENTS.md\` at the repo root — the conventions. \`.claude/\` is only the harness that reads them.
124
+ `;
125
+
126
+ /** The agent harness for a new app: three commands, four boundary agents, settings, and a map. */
127
+ export function claudeFiles(app: NameSet): readonly GeneratedFile[] {
128
+ return [
129
+ { path: '.claude/README.md', contents: readme(app) },
130
+ { path: '.claude/settings.json', contents: settings() },
131
+ ...claudeCommandFiles(app),
132
+ ...claudeAgentFiles(app),
133
+ ];
134
+ }
@@ -91,6 +91,11 @@ const composeProd = (
91
91
  # What \`x deploy --method compose\` runs. migrate runs to completion before anything serves.
92
92
  #
93
93
  # IMAGE=ghcr.io/you/${app.kebab}:1.2.3 x deploy --image ghcr.io/you/${app.kebab}:1.2.3
94
+ #
95
+ # A published host port has exactly one binder, so \`web\` and \`sync\` run at 1 here. Compose is one
96
+ # box; horizontal scaling of those two belongs to an orchestrator (copy \`docker/helm\` from the
97
+ # framework repo). To scale them on one box anyway, drop \`ports:\` and put your own proxy on this
98
+ # network — the service name resolves to every replica over the compose DNS round robin.
94
99
  name: ${app.kebab}
95
100
 
96
101
  x-image: &image
@@ -120,13 +125,33 @@ services:
120
125
  environment: [ROLE=migrate]
121
126
  restart: 'no'
122
127
 
128
+ # Run-once, AFTER the new version serves. Deliberately NOT part of the release gate: a slow
129
+ # UPDATE there holds the deploy open against a database still serving the previous version.
130
+ # Dry run is the default, so \`--write\` is explicit.
131
+ backfill:
132
+ <<: *image
133
+ # The image's ENTRYPOINT is \`bun apps/web/server.ts\`, and that entry reads ROLE and PORT and
134
+ # NOTHING ELSE — argv never reaches a parser. A bare \`command:\` is appended to it and silently
135
+ # discarded, so this service used to serve HTTP as ROLE=web under a name that said otherwise.
136
+ # Overriding the entrypoint is what makes the words below a command. The file path, not
137
+ # \`node_modules/.bin/x\`: it needs no bin symlink and no executable bit inside the image.
138
+ entrypoint: ['bun', 'node_modules/@ultimat3/cli/src/bin.ts']
139
+ command: ['db', 'backfill', '--all', '--write', '--json']
140
+ depends_on:
141
+ db: { condition: service_healthy }
142
+ migrate: { condition: service_completed_successfully }
143
+ # The barrier, not the ordering. \`docker compose up -d\` returns when a container STARTS, so
144
+ # listing this last would only look like "after". The image's HEALTHCHECK is what makes it true.
145
+ web: { condition: service_healthy }
146
+ restart: 'no'
147
+
123
148
  web:
124
149
  <<: *image
125
150
  environment: [ROLE=web]
126
151
  depends_on:
127
152
  db: { condition: service_healthy }
128
153
  migrate: { condition: service_completed_successfully }
129
- deploy: { replicas: 2 } # stateless: scales on RPS
154
+ deploy: { replicas: 1 } # stateless, scales on RPS — pinned by the published port
130
155
  ports: ['3000:3000']
131
156
 
132
157
  sync:
@@ -135,7 +160,8 @@ services:
135
160
  depends_on:
136
161
  db: { condition: service_healthy }
137
162
  migrate: { condition: service_completed_successfully }
138
- deploy: { replicas: 1 } # scales on concurrent websockets; no sticky sessions
163
+ deploy: { replicas: 1 } # scales on concurrent websockets, no sticky sessions — pinned by the port
164
+ # The sync role binds PORT + 1. PORT is unset here, so it is 3000 and this listens on 3001.
139
165
  ports: ['3001:3001']
140
166
 
141
167
  worker:
@@ -190,6 +216,11 @@ docker compose -f docker/docker-compose.prod.yml up -d # db → migrate →
190
216
  x deploy --image ${app.kebab}:dev --dry-run --json # the same plan, printed
191
217
  \`\`\`
192
218
 
219
+ \`web\` and \`sync\` publish a host port, so both sit at \`replicas: 1\`: one host port has exactly one
220
+ binder, and a second container dies on \`port is already allocated\`. \`worker\` publishes nothing and
221
+ scales freely. To scale the two serving roles on one box, delete their \`ports:\` lines and put your
222
+ own proxy on the compose network. To scale them properly, use an orchestrator — see below.
223
+
193
224
  ## The other two build targets
194
225
 
195
226
  \`\`\`sh
@@ -235,6 +266,19 @@ that no longer matches an applied migration stops the release instead of corrupt
235
266
  There is no \`x db migrate\` in that list on purpose: it is the developer's command and it needs the
236
267
  toolchain, while the release phase runs the shipped image and nothing else.
237
268
 
269
+ ### One-off commands need a new entrypoint, not arguments
270
+
271
+ \`ENTRYPOINT\` is \`bun apps/web/server.ts\`, and that entry reads \`ROLE\` and \`PORT\` and **nothing
272
+ else** — argv never reaches a parser. So arguments appended to it are discarded in silence:
273
+
274
+ \`\`\`sh
275
+ docker run ${app.kebab}:dev db backfill --all --write # serves ROLE=web, forever
276
+ docker run --entrypoint bun ${app.kebab}:dev node_modules/@ultimat3/cli/src/bin.ts db backfill --all --write --json
277
+ \`\`\`
278
+
279
+ The \`backfill\` service in \`docker-compose.prod.yml\` is the second form. A Kubernetes \`Job\` running
280
+ a one-off command sets \`command:\` (the entrypoint) as well as \`args:\`, for the same reason.
281
+
238
282
  ## Environment
239
283
 
240
284
  | Key | Meaning | Unset means |
@@ -0,0 +1,91 @@
1
+ // The generated app's `packages/db`: the entity re-export list the migration generator reads and
2
+ // the deterministic seed. No business logic — that is the package's own stated boundary, and it is
3
+ // why `example` reaches only the two files describing the slice's table.
4
+ //
5
+ // No migration. `x db gen` is the ONE writer of `packages/db/migrations`, and a scaffold that hand-
6
+ // wrote `0000_initial.sql` was a second one: it declared a `posts` table the generator had never
7
+ // diffed, so the first `x db gen` saw a schema the ledger already claimed and the two disagreed
8
+ // about what "initial" meant. `x db gen "initial"` is the app's first command instead — it writes
9
+ // the `.sql`, the `.snapshot.json` and the `.hash` together, which no hand-written file can.
10
+
11
+ import type { GeneratedFile, NameSet } from './naming';
12
+ import { packageShapeFiles, workspacePackageJson } from './scaffold-package-shape';
13
+
14
+ const DESCRIPTION = 'Entity re-exports and SQL migrations, no business logic';
15
+
16
+ const dbIndex =
17
+ (): string => `// Schema and migrations only — no business logic lives in this package. The client itself is
18
+ // @ultimat3/db's: one connection pool, sized by ROLE, shared by every package in the app.
19
+ export type { DbClient, SqlFragment } from '@ultimat3/db';
20
+ export { db, sql, withTransaction } from '@ultimat3/db';
21
+ export * as schema from './schema';
22
+ `;
23
+
24
+ // The four pieces below describe the example slice's table. Under `--no-example` that slice is
25
+ // never written, so each one ships its empty counterpart instead of a reference to a file that is
26
+ // not there — `export { post } from …` alone made `x new --no-example` an app that cannot compile.
27
+
28
+ const SCHEMA_HEADER = `// Every entity the app declares, re-exported here. This list is what the migration generator
29
+ // reads, so an entity that is not exported here does not exist as far as the database is concerned.`;
30
+
31
+ /**
32
+ * `bun run db:seed`'s entry point. Identical either way — only the rows differ. Interpolated, not
33
+ * nested, so it carries exactly the escaping a single template literal needs.
34
+ */
35
+ const SEED_MAIN = `
36
+
37
+ if (import.meta.main) {
38
+ const count = await seed();
39
+ // Bun's stdout, not process.stdout: one runtime, one API. Awaited because the write resolves
40
+ // asynchronously, and this JSON line is the whole output of \`bun run db:seed\`.
41
+ await Bun.stdout.write(\`\${JSON.stringify({ ok: true, seeded: count })}\\n\`);
42
+ }
43
+ `;
44
+
45
+ const dbSchema = (app: NameSet, example: boolean): string =>
46
+ example
47
+ ? `${SCHEMA_HEADER}
48
+ export { post } from '@${app.kebab}/web/app/post/entity';
49
+ `
50
+ : `${SCHEMA_HEADER}
51
+ // \`x g entity <name>\` writes the entity; add its export here so the database learns about it.
52
+ export {};
53
+ `;
54
+
55
+ const dbSeed = (app: NameSet, example: boolean): string =>
56
+ example
57
+ ? `// Deterministic seed: same rows every time, so a test and a demo see the same database.
58
+ import { db, sql } from '@ultimat3/db';
59
+
60
+ const ORG = '00000000-0000-0000-0000-000000000002';
61
+
62
+ export async function seed(): Promise<number> {
63
+ const rows = [
64
+ { id: '00000000-0000-0000-0000-000000000101', title: 'Hello ${app.pascal}', minor: 0 },
65
+ { id: '00000000-0000-0000-0000-000000000102', title: 'Second post', minor: 1900 },
66
+ ];
67
+ for (const row of rows) {
68
+ // Idempotent by primary key, so re-seeding a branch database is a no-op rather than a crash.
69
+ await db().execute(sql\`
70
+ insert into posts (id, org_id, title, price_minor, price_currency)
71
+ values (\${row.id}, \${ORG}, \${row.title}, \${row.minor}, 'USD')
72
+ on conflict (id) do nothing\`);
73
+ }
74
+ return rows.length;
75
+ }${SEED_MAIN}`
76
+ : `// Deterministic seed: same rows every time, so a test and a demo see the same database.
77
+ // No entity is declared yet, so there is nothing to insert — the shape stays, so the first
78
+ // \`x g entity\` has one obvious place to seed from.
79
+
80
+ export async function seed(): Promise<number> {
81
+ return 0;
82
+ }${SEED_MAIN}`;
83
+
84
+ /** Every file the `packages/db` workspace ships, in the order `x new` writes them. */
85
+ export const dbPackageFiles = (app: NameSet, example: boolean): readonly GeneratedFile[] => [
86
+ { path: 'packages/db/package.json', contents: workspacePackageJson(app, 'db', DESCRIPTION) },
87
+ ...packageShapeFiles(app, 'db', DESCRIPTION),
88
+ { path: 'packages/db/src/index.ts', contents: dbIndex() },
89
+ { path: 'packages/db/src/schema.ts', contents: dbSchema(app, example) },
90
+ { path: 'packages/db/src/seed.ts', contents: dbSeed(app, example) },
91
+ ];
@@ -1,9 +1,11 @@
1
1
  // The human-authored half of what `x new` writes: the READMEs, the agent-facing convention files,
2
2
  // the bin/ shims and the optional dev compose. Separated from the config half so neither file has
3
3
  // to be scrolled to find the other — one file, one job applies to templates too. The image, its
4
- // ignore file, the production topology and the deploy page are `scaffold-container.ts`.
4
+ // ignore file, the production topology and the deploy page are `scaffold-container.ts`; the
5
+ // `.claude/` harness that reads AGENTS.md is `scaffold-claude.ts`.
5
6
 
6
7
  import type { GeneratedFile, NameSet } from './naming';
8
+ import { claudeFiles } from './scaffold-claude';
7
9
  import { containerFiles } from './scaffold-container';
8
10
 
9
11
  const agents = (app: NameSet): string => `# AGENTS.md
@@ -23,7 +25,7 @@ agent cannot infer from the code.
23
25
  | Strings | every user-facing string goes through \`t()\` |
24
26
  | Colour | semantic tokens only, never a raw hex |
25
27
 
26
- Commands: \`x dev\`, \`x verify\`, \`x g <primitive>\`, \`x db branch <name>\`, \`x doctor\`.
28
+ Commands: \`x dev\`, \`x verify\`, \`x g <primitive>\`, \`x db branch create <name>\`, \`x doctor\`.
27
29
 
28
30
  Project notes for ${app.kebab}: replace this line with the conventions a newcomer could not guess.
29
31
  `;
@@ -33,9 +35,14 @@ const claude = (app: NameSet): string => `# CLAUDE.md
33
35
  ${app.kebab} — Ultimate app. Read AGENTS.md first; it is the same content in the same order.
34
36
 
35
37
  - Gate: \`x verify\` (add \`--json\` for machine output).
36
- - Scaffold, do not hand-write: \`x g resource|action|job|route|policy|entity|query|task\`.
37
- - Destructive DB work goes in a branch: \`x db branch <name>\`, never the shared dev DB.
38
+ - Scaffold, do not hand-write: \`x g <kind> <name>\` — \`x g --help\` lists every kind, and is the
39
+ only place that list is stated.
40
+ - Destructive DB work goes in a branch: \`x db branch create <name>\`, never the shared dev DB.
38
41
  - \`x doctor\` explains a broken environment and prints the fix command for every finding.
42
+
43
+ \`.claude/\` holds the harness that reads this file: \`/feature\`, \`/planx\`, \`/verify\` and four
44
+ boundary-scoped subagents. It is yours — \`.claude/README.md\` says what each one costs, and every
45
+ file in it is deletable.
39
46
  `;
40
47
 
41
48
  const readme = (app: NameSet): string => `# ${app.pascal}
@@ -45,11 +52,15 @@ Built with [Ultimate](https://ultimate.dev). Bun-only, Postgres, SolidJS.
45
52
  ## 🚀 Start
46
53
 
47
54
  \`\`\`sh
48
- bin/setup # prerequisites, deps, env, migrate, seed
55
+ bin/setup # prerequisites, deps, env, the first migration, migrate, seed
49
56
  x dev # all roles in one process, embedded Postgres, /_x mounted
50
57
  x verify # the gate: typecheck, lint, boundaries, tests, drift, budgets
51
58
  \`\`\`
52
59
 
60
+ \`packages/db/migrations\` starts empty and \`x db gen\` is its only writer — \`bin/setup\` runs
61
+ \`x db gen "initial"\` for you on a fresh clone. Until it has, \`x verify\`'s \`drift\` step is red
62
+ with \`X_DB_DRIFT\`, and that is the fix it names.
63
+
53
64
  ## 🗺 Layout
54
65
 
55
66
  | Path | Holds |
@@ -71,6 +82,11 @@ cd "$(dirname "$0")/.."
71
82
  command -v bun >/dev/null || { echo "X_BUN_MISSING: install bun — https://bun.sh"; exit 1; }
72
83
  bun install
73
84
  [ -f .env.development.local ] || printf '# per-box secrets, gitignored, wins over .env.development\\n' > .env.development.local
85
+ # \`x db gen\` is the ONE writer of packages/db/migrations — the scaffold no longer hand-writes a
86
+ # 0000_initial.sql, because a second writer is how the source and the ledger ended up disagreeing
87
+ # about what "initial" meant. Guarded on the directory rather than on the generator being a no-op:
88
+ # this script is documented idempotent, and the guard is what makes that true here.
89
+ ls packages/db/migrations/*.sql >/dev/null 2>&1 || bunx x db gen "initial"
74
90
  bunx x db migrate "$@"
75
91
  bun run db:seed
76
92
  echo "setup complete — next: x dev"
@@ -126,6 +142,9 @@ export function docsFiles(app: NameSet): readonly GeneratedFile[] {
126
142
  { path: 'bin/dev', contents: binDev() },
127
143
  { path: 'bin/check', contents: binCheck() },
128
144
  { path: 'docker/docker-compose.dev.yml', contents: composeDev(app) },
145
+ // The harness half of the same job AGENTS.md does. It lands in the app's own repo rather than
146
+ // in a global config, so it is visible in the scaffold's diff and deletable in one line.
147
+ ...claudeFiles(app),
129
148
  ...containerFiles(app),
130
149
  ];
131
150
  }