@obh/forge 0.2.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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/dist/capabilities/analytics.d.ts +3 -0
  4. package/dist/capabilities/analytics.d.ts.map +1 -0
  5. package/dist/capabilities/analytics.js +91 -0
  6. package/dist/capabilities/analytics.js.map +1 -0
  7. package/dist/capabilities/api-keys.d.ts +3 -0
  8. package/dist/capabilities/api-keys.d.ts.map +1 -0
  9. package/dist/capabilities/api-keys.js +90 -0
  10. package/dist/capabilities/api-keys.js.map +1 -0
  11. package/dist/capabilities/audit.d.ts +3 -0
  12. package/dist/capabilities/audit.d.ts.map +1 -0
  13. package/dist/capabilities/audit.js +74 -0
  14. package/dist/capabilities/audit.js.map +1 -0
  15. package/dist/capabilities/entitlements.d.ts +3 -0
  16. package/dist/capabilities/entitlements.d.ts.map +1 -0
  17. package/dist/capabilities/entitlements.js +94 -0
  18. package/dist/capabilities/entitlements.js.map +1 -0
  19. package/dist/capabilities/events.d.ts +3 -0
  20. package/dist/capabilities/events.d.ts.map +1 -0
  21. package/dist/capabilities/events.js +107 -0
  22. package/dist/capabilities/events.js.map +1 -0
  23. package/dist/capabilities/files.d.ts +3 -0
  24. package/dist/capabilities/files.d.ts.map +1 -0
  25. package/dist/capabilities/files.js +100 -0
  26. package/dist/capabilities/files.js.map +1 -0
  27. package/dist/capabilities/helpers.d.ts +27 -0
  28. package/dist/capabilities/helpers.d.ts.map +1 -0
  29. package/dist/capabilities/helpers.js +63 -0
  30. package/dist/capabilities/helpers.js.map +1 -0
  31. package/dist/capabilities/import-export.d.ts +3 -0
  32. package/dist/capabilities/import-export.d.ts.map +1 -0
  33. package/dist/capabilities/import-export.js +267 -0
  34. package/dist/capabilities/import-export.js.map +1 -0
  35. package/dist/capabilities/index.d.ts +16 -0
  36. package/dist/capabilities/index.d.ts.map +1 -0
  37. package/dist/capabilities/index.js +71 -0
  38. package/dist/capabilities/index.js.map +1 -0
  39. package/dist/capabilities/jobs.d.ts +3 -0
  40. package/dist/capabilities/jobs.d.ts.map +1 -0
  41. package/dist/capabilities/jobs.js +117 -0
  42. package/dist/capabilities/jobs.js.map +1 -0
  43. package/dist/capabilities/notifications.d.ts +3 -0
  44. package/dist/capabilities/notifications.d.ts.map +1 -0
  45. package/dist/capabilities/notifications.js +113 -0
  46. package/dist/capabilities/notifications.js.map +1 -0
  47. package/dist/capabilities/search.d.ts +3 -0
  48. package/dist/capabilities/search.d.ts.map +1 -0
  49. package/dist/capabilities/search.js +121 -0
  50. package/dist/capabilities/search.js.map +1 -0
  51. package/dist/capabilities/settings.d.ts +3 -0
  52. package/dist/capabilities/settings.d.ts.map +1 -0
  53. package/dist/capabilities/settings.js +75 -0
  54. package/dist/capabilities/settings.js.map +1 -0
  55. package/dist/capabilities/types.d.ts +16 -0
  56. package/dist/capabilities/types.d.ts.map +1 -0
  57. package/dist/capabilities/types.js +3 -0
  58. package/dist/capabilities/types.js.map +1 -0
  59. package/dist/capabilities/webhooks.d.ts +3 -0
  60. package/dist/capabilities/webhooks.d.ts.map +1 -0
  61. package/dist/capabilities/webhooks.js +117 -0
  62. package/dist/capabilities/webhooks.js.map +1 -0
  63. package/dist/cli.d.ts +3 -0
  64. package/dist/cli.d.ts.map +1 -0
  65. package/dist/cli.js +60 -0
  66. package/dist/cli.js.map +1 -0
  67. package/dist/commands/add.d.ts +3 -0
  68. package/dist/commands/add.d.ts.map +1 -0
  69. package/dist/commands/add.js +88 -0
  70. package/dist/commands/add.js.map +1 -0
  71. package/dist/commands/doctor.d.ts +3 -0
  72. package/dist/commands/doctor.d.ts.map +1 -0
  73. package/dist/commands/doctor.js +94 -0
  74. package/dist/commands/doctor.js.map +1 -0
  75. package/dist/commands/generate.d.ts +3 -0
  76. package/dist/commands/generate.d.ts.map +1 -0
  77. package/dist/commands/generate.js +41 -0
  78. package/dist/commands/generate.js.map +1 -0
  79. package/dist/commands/inspect.d.ts +3 -0
  80. package/dist/commands/inspect.d.ts.map +1 -0
  81. package/dist/commands/inspect.js +64 -0
  82. package/dist/commands/inspect.js.map +1 -0
  83. package/dist/commands/new.d.ts +18 -0
  84. package/dist/commands/new.d.ts.map +1 -0
  85. package/dist/commands/new.js +149 -0
  86. package/dist/commands/new.js.map +1 -0
  87. package/dist/commands/shared.d.ts +14 -0
  88. package/dist/commands/shared.d.ts.map +1 -0
  89. package/dist/commands/shared.js +36 -0
  90. package/dist/commands/shared.js.map +1 -0
  91. package/dist/commands/skill.d.ts +3 -0
  92. package/dist/commands/skill.d.ts.map +1 -0
  93. package/dist/commands/skill.js +59 -0
  94. package/dist/commands/skill.js.map +1 -0
  95. package/dist/commands/stack.d.ts +13 -0
  96. package/dist/commands/stack.d.ts.map +1 -0
  97. package/dist/commands/stack.js +192 -0
  98. package/dist/commands/stack.js.map +1 -0
  99. package/dist/generators/api.d.ts +16 -0
  100. package/dist/generators/api.d.ts.map +1 -0
  101. package/dist/generators/api.js +375 -0
  102. package/dist/generators/api.js.map +1 -0
  103. package/dist/generators/lwd.d.ts +12 -0
  104. package/dist/generators/lwd.d.ts.map +1 -0
  105. package/dist/generators/lwd.js +143 -0
  106. package/dist/generators/lwd.js.map +1 -0
  107. package/dist/generators/mobile.d.ts +16 -0
  108. package/dist/generators/mobile.d.ts.map +1 -0
  109. package/dist/generators/mobile.js +180 -0
  110. package/dist/generators/mobile.js.map +1 -0
  111. package/dist/generators/platform-package.d.ts +18 -0
  112. package/dist/generators/platform-package.d.ts.map +1 -0
  113. package/dist/generators/platform-package.js +568 -0
  114. package/dist/generators/platform-package.js.map +1 -0
  115. package/dist/generators/root.d.ts +15 -0
  116. package/dist/generators/root.d.ts.map +1 -0
  117. package/dist/generators/root.js +249 -0
  118. package/dist/generators/root.js.map +1 -0
  119. package/dist/generators/sdk.d.ts +14 -0
  120. package/dist/generators/sdk.d.ts.map +1 -0
  121. package/dist/generators/sdk.js +137 -0
  122. package/dist/generators/sdk.js.map +1 -0
  123. package/dist/generators/web.d.ts +15 -0
  124. package/dist/generators/web.d.ts.map +1 -0
  125. package/dist/generators/web.js +203 -0
  126. package/dist/generators/web.js.map +1 -0
  127. package/dist/generators/worker.d.ts +13 -0
  128. package/dist/generators/worker.d.ts.map +1 -0
  129. package/dist/generators/worker.js +148 -0
  130. package/dist/generators/worker.js.map +1 -0
  131. package/dist/index.d.ts +10 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +44 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/project/context.d.ts +24 -0
  136. package/dist/project/context.d.ts.map +1 -0
  137. package/dist/project/context.js +63 -0
  138. package/dist/project/context.js.map +1 -0
  139. package/dist/project/manifest.d.ts +26 -0
  140. package/dist/project/manifest.d.ts.map +1 -0
  141. package/dist/project/manifest.js +73 -0
  142. package/dist/project/manifest.js.map +1 -0
  143. package/dist/project/paths.d.ts +25 -0
  144. package/dist/project/paths.d.ts.map +1 -0
  145. package/dist/project/paths.js +51 -0
  146. package/dist/project/paths.js.map +1 -0
  147. package/dist/project/plan.d.ts +42 -0
  148. package/dist/project/plan.d.ts.map +1 -0
  149. package/dist/project/plan.js +251 -0
  150. package/dist/project/plan.js.map +1 -0
  151. package/dist/prompts/index.d.ts +24 -0
  152. package/dist/prompts/index.d.ts.map +1 -0
  153. package/dist/prompts/index.js +111 -0
  154. package/dist/prompts/index.js.map +1 -0
  155. package/dist/recipes/index.d.ts +25 -0
  156. package/dist/recipes/index.d.ts.map +1 -0
  157. package/dist/recipes/index.js +77 -0
  158. package/dist/recipes/index.js.map +1 -0
  159. package/dist/stack/infer.d.ts +4 -0
  160. package/dist/stack/infer.d.ts.map +1 -0
  161. package/dist/stack/infer.js +165 -0
  162. package/dist/stack/infer.js.map +1 -0
  163. package/dist/stack/lwd.d.ts +24 -0
  164. package/dist/stack/lwd.d.ts.map +1 -0
  165. package/dist/stack/lwd.js +62 -0
  166. package/dist/stack/lwd.js.map +1 -0
  167. package/dist/stack/lwdtoml.d.ts +38 -0
  168. package/dist/stack/lwdtoml.d.ts.map +1 -0
  169. package/dist/stack/lwdtoml.js +57 -0
  170. package/dist/stack/lwdtoml.js.map +1 -0
  171. package/dist/stack/manifest.d.ts +15 -0
  172. package/dist/stack/manifest.d.ts.map +1 -0
  173. package/dist/stack/manifest.js +86 -0
  174. package/dist/stack/manifest.js.map +1 -0
  175. package/dist/stack/types.d.ts +48 -0
  176. package/dist/stack/types.d.ts.map +1 -0
  177. package/dist/stack/types.js +5 -0
  178. package/dist/stack/types.js.map +1 -0
  179. package/dist/stack/wire.d.ts +40 -0
  180. package/dist/stack/wire.d.ts.map +1 -0
  181. package/dist/stack/wire.js +192 -0
  182. package/dist/stack/wire.js.map +1 -0
  183. package/dist/types.d.ts +114 -0
  184. package/dist/types.d.ts.map +1 -0
  185. package/dist/types.js +5 -0
  186. package/dist/types.js.map +1 -0
  187. package/dist/utils/fs.d.ts +13 -0
  188. package/dist/utils/fs.d.ts.map +1 -0
  189. package/dist/utils/fs.js +64 -0
  190. package/dist/utils/fs.js.map +1 -0
  191. package/dist/utils/git.d.ts +17 -0
  192. package/dist/utils/git.d.ts.map +1 -0
  193. package/dist/utils/git.js +49 -0
  194. package/dist/utils/git.js.map +1 -0
  195. package/dist/utils/logger.d.ts +16 -0
  196. package/dist/utils/logger.d.ts.map +1 -0
  197. package/dist/utils/logger.js +21 -0
  198. package/dist/utils/logger.js.map +1 -0
  199. package/dist/utils/shell.d.ts +21 -0
  200. package/dist/utils/shell.d.ts.map +1 -0
  201. package/dist/utils/shell.js +40 -0
  202. package/dist/utils/shell.js.map +1 -0
  203. package/dist/version.d.ts +2 -0
  204. package/dist/version.d.ts.map +1 -0
  205. package/dist/version.js +6 -0
  206. package/dist/version.js.map +1 -0
  207. package/package.json +40 -0
  208. package/skills/obh-add-events/SKILL.md +26 -0
  209. package/skills/obh-generate-audit-rules/SKILL.md +26 -0
  210. package/skills/obh-inspect-project/SKILL.md +26 -0
  211. package/skills/obh-lwd-manifest/SKILL.md +26 -0
  212. package/skills/obh-retrofit-files/SKILL.md +24 -0
  213. package/skills/obh-retrofit-jobs/SKILL.md +26 -0
  214. package/skills/obh-sdk-extraction/SKILL.md +24 -0
  215. package/skills/obh-settings-migration/SKILL.md +24 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":";;;AAAA,wEAAwE;AAC3D,QAAA,aAAa,GAAG,OAAO,CAAA"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@obh/forge",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "OBH Forge — deterministic scaffolding and delivery tooling for OBH software projects.",
6
+ "license": "MIT",
7
+ "packageManager": "pnpm@9.7.0",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "type": "commonjs",
12
+ "bin": {
13
+ "forge": "dist/cli.js",
14
+ "obh-forge": "dist/cli.js"
15
+ },
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "files": [
19
+ "dist",
20
+ "skills"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "typecheck": "tsc -p tsconfig.json --noEmit",
25
+ "test": "vitest run",
26
+ "start": "node dist/cli.js"
27
+ },
28
+ "dependencies": {
29
+ "@iarna/toml": "^2.2.5",
30
+ "commander": "^12.1.0",
31
+ "picocolors": "^1.0.1",
32
+ "prompts": "^2.4.2"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.14.0",
36
+ "@types/prompts": "^2.4.9",
37
+ "typescript": "^5.5.4",
38
+ "vitest": "^2.0.5"
39
+ }
40
+ }
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: obh-add-events
3
+ description: Use when adding an event catalogue to a project's domain — turning writes into emitted facts. Inspects routes/services, proposes dot.notation event names with payloads and exact in-transaction emit sites, then guides `forge add events` and wiring emits into the app event bus.
4
+ ---
5
+
6
+ Purpose: give a project a coherent set of domain **events** (facts) so other primitives can react, without inventing events that don't map to real state changes. Events are facts in `dot.notation` (e.g. `note.created`); they are emitted inside the write transaction that produced them.
7
+
8
+ ## Workflow
9
+
10
+ 1. **Find the writes.** Inspect `apps/api/src/routes/*.ts` and `apps/api/src/domain/*` for handlers that INSERT/UPDATE/DELETE or change state. Every meaningful state transition is a candidate fact. Ignore pure reads.
11
+
12
+ 2. **Name events as facts.** Use `resource.pastTenseVerb` in dot.notation: `note.created`, `note.updated`, `note.deleted`, `invoice.paid`, `user.invited`. State changes get their own event (`order.shipped`), not a generic `order.updated`, when a consumer would care about the specific transition.
13
+
14
+ 3. **Design payloads.** Each payload carries the IDs and the minimal facts a consumer needs — resource id, actor id, workspace/tenant id, and the fields that changed (or before/after for updates). Keep payloads flat and serialisable; reference large blobs by id, don't inline them. Avoid leaking secrets or full PII into payloads that audit/analytics will persist.
15
+
16
+ 4. **Pin the transaction boundary.** For each event, identify the exact write transaction and place the emit **inside** it (same tx/connection as the DB write) so the fact and the state commit atomically via the outbox. Note the precise call-site (file + function + line region). If a handler does multiple writes, decide whether it emits one event or several.
17
+
18
+ 5. **Map future consumers.** For each event, list which primitives should react: **notifications** (user-facing email/rules), **audit** (immutable trail), **analytics** (KPIs), **search** (index updates), **webhooks** (outbound), **jobs** (follow-up commands). This drives later `forge add` choices — don't wire them yet, just record intent.
19
+
20
+ 6. **Install the primitive.** Run `forge add events` (use `--dry-run` first). This scaffolds the outbox/dispatcher migration in `scripts/migrations.d/*`, the `@obh/events` client, and the in-app bus at `apps/api/src/bus.ts`. Run `pnpm migrate` to apply the platform schema.
21
+
22
+ 7. **Wire the emits.** In each write handler, call `bus.emit('note.created', payload)` inside the tx. Auto-loaded subscribers live in `apps/api/src/bus.d/*` — add a subscriber file per reacting concern (they run in-process; heavy work should enqueue a job rather than block the request). Use `defineX`/`createXClient` grammar from `@obh/events` for the typed event definitions.
23
+
24
+ ## Output
25
+
26
+ An **event catalogue**: a table of `event.name` → payload shape → emitting file/tx site → intended consumers; the `forge add events` command (with `--dry-run` note) and `pnpm migrate` step; and a per-handler wiring plan showing where `bus.emit(...)` goes inside each transaction and which `bus.d/*` subscribers to create. Flag any write that currently emits nothing but should.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: obh-generate-audit-rules
3
+ description: Use when a project has domain events and needs an immutable audit trail. Generates @obh/audit rule mappings (event -> action/target/actor) covering create/update/delete/state-change events, with redaction and immutability notes.
4
+ ---
5
+
6
+ Purpose: turn existing domain **events** (facts) into an **audit** trail. The audit primitive derives an immutable log from events — each rule maps one event to an audit record's action, target, and actor. Audit does not observe the DB directly; it reacts to events, so this skill depends on an event catalogue already existing (see obh-add-events).
7
+
8
+ ## Workflow
9
+
10
+ 1. **Collect the events.** Read the project's event definitions (from `@obh/events` `defineX` files / the catalogue). List every event name, its payload, and what state change it represents. If no events exist yet, stop and run obh-add-events first — there is nothing for audit to observe.
11
+
12
+ 2. **Classify each event.** Bucket into create / update / delete / state-change. Every one of these is auditable. Pure read or ephemeral events (e.g. `session.pinged`) usually are not — note them as excluded with a reason.
13
+
14
+ 3. **Derive action/target/actor per rule.**
15
+ - **action** — a stable verb phrase from the event: `note.created` → `note.create`, `invoice.paid` → `invoice.mark_paid`. Keep actions consistent across resources.
16
+ - **target** — the entity acted on: type + id pulled from the payload (`{ type: 'note', id: payload.noteId }`).
17
+ - **actor** — who did it: user/api-key/system id from the payload. If the payload lacks an actor, flag it — the event may need to carry `actorId`.
18
+ - For **update** events, capture a diff (changed fields, or before/after) as audit metadata so the trail is meaningful.
19
+
20
+ 4. **Plan redaction.** Mark payload fields that must not land in the immutable log: secrets, tokens, full PII, large blobs. Specify per-rule redaction (drop or hash) before the audit record is written — remember records are immutable, so a leak can't be edited out later.
21
+
22
+ 5. **Install and wire.** Run `forge add audit` (`--dry-run` first). This adds the audit migration to `scripts/migrations.d/*` and the `@obh/audit` client. Run `pnpm migrate`. Register the rules against the event bus/subscribers (`apps/api/src/bus.d/*` or the audit worker consumer) so each event produces its audit record. Audit writes are append-only — never expose update/delete on them.
23
+
24
+ ## Output
25
+
26
+ An **audit rule map**: a table of `event.name` → `action` → target (type+id source) → actor (source) → captured metadata/diff → redacted fields, plus an **excluded events** list with reasons; the `forge add audit` + `pnpm migrate` steps; where rules are registered; and an explicit immutability/redaction note (records are append-only; redact before write).
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: obh-inspect-project
3
+ description: Use when you need to understand an unfamiliar or pre-Forge codebase before changing it. Reads the repo and reports project shape, DB and deploy patterns, which OBH platform primitives are present vs. candidates, risks, and a recommended `forge add` sequence.
4
+ ---
5
+
6
+ Purpose: build an accurate map of a repository so subsequent Forge/retrofit work is grounded in what actually exists, not assumptions. This skill only reads and reports — it makes no changes.
7
+
8
+ ## Workflow
9
+
10
+ 1. **Detect project shape.** Look for `forge.json` first — if present, read the recorded apps, packages, installed primitives, and topology; the repo is already Forge-managed. Otherwise infer: check for `pnpm-workspace.yaml` + `apps/*` + `packages/*` (Forge-style monorepo) vs. a single-package app. List every app and package with its role (api, admin, worker, mobile, sdk, ui, config).
11
+
12
+ 2. **Identify the package manager.** `pnpm-lock.yaml` → pnpm (Forge default), `package-lock.json` → npm, `yarn.lock` → yarn. Note if mixed lockfiles exist (a risk).
13
+
14
+ 3. **Determine the DB access pattern.** Grep for: raw `pg` / `Pool` / `client.query` (raw SQL — Forge-native); `typeorm`, `@mikro-orm`, `prisma`, `sequelize`, `drizzle` (ORM). Record whether schema is managed by migrations (`migrations/*.sql`, `scripts/migrate.ts`) or by ORM `synchronize`/auto-sync. Note the schemas in use — product tables belong in `public`, OBH primitives own `platform`.
15
+
16
+ 4. **Map deployment.** Look for `deploy/*.lwd.toml` (Forge/lwd), `Dockerfile`, `docker-compose.yml`, `vercel.json`, `fly.toml`, k8s manifests, CI workflows. Note where secrets live (committed `.env`, CI secrets, lwd secret names).
17
+
18
+ 5. **Census platform primitives.** For each OBH primitive (events, jobs, files, audit, settings, api-keys, webhooks, import-export, entitlements, search, analytics, notifications), decide **present** (an `@obh/*` dep or a `scripts/migrations.d/*` entry) vs. **candidate** (hand-rolled equivalent exists — e.g. a homegrown outbox table, a cron route, multer uploads, a `settings`/`options` table). Cite the file that triggered each candidate.
19
+
20
+ 6. **Flag risks.** Watch for: ORM auto-sync/`synchronize: true` (schema drift, no reviewable migrations); missing migrations directory; no `/health` or readiness endpoint; duplicated types between backend and frontend(s); committed secrets; missing event seam (writes with no facts emitted); a worker surface mixed into the API process.
21
+
22
+ 7. **Recommend a `forge add` sequence.** Order by dependency: `events` first (most primitives react to events), then the primitives that consume them (`audit`, `analytics`, `notifications`, `search`), then independent ones (`files`, `jobs`, `settings`, `api-keys`, `webhooks`, `import-export`, `entitlements`). Only recommend primitives justified by a real candidate. Suggest `forge inspect` and `forge doctor` to validate before mutating, and note every mutating command supports `--dry-run`.
23
+
24
+ ## Output
25
+
26
+ A concise report with sections: **Shape** (apps/packages + package manager), **Data** (DB access + migration strategy + schemas), **Deploy** (surfaces + secrets), **Primitives** (present vs. candidate table, each candidate citing a file), **Risks** (ranked), and **Recommended sequence** (an ordered list of `forge add <primitive>` commands with a one-line reason each, prefixed with a `forge doctor` / `--dry-run` note). No code changes.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: obh-lwd-manifest
3
+ description: Use when generating or updating lwd deploy manifests for a Forge project. Encodes real lwd constraints — worker as a separate surface, no co-located Postgres on a replicated API, per-app network isolation, secret NAMES only — and prefers `forge generate lwd` over hand-editing.
4
+ ---
5
+
6
+ Purpose: produce correct `deploy/*.lwd.toml` manifests that match the app topology. Prefer generating them with Forge; hand-edit only for constraints the generator can't infer. lwd manifests reference secret **NAMES** only — values are set out-of-band with `lwd secret set`.
7
+
8
+ ## Workflow
9
+
10
+ 1. **Read the topology.** From `forge.json` and `apps/*`, list the deployable surfaces: api, admin/web, worker. Note replica intent (is the API meant to scale, `replicas > 1`?) and whether a database is expected to be managed by lwd or external.
11
+
12
+ 2. **One surface per manifest, correctly typed.** Each app gets its own `deploy/<app>.lwd.toml`. A **worker is a separate surface** from the API — it needs its own domain/entrypoint and its **own health check** (a liveness signal from the tick loop, not the API's HTTP `/health`). Don't fold the worker into the API manifest.
13
+
14
+ 3. **Apply the Postgres co-location rule.** A `[[services]]` Postgres may be co-located only with a single-instance surface. If the API is scalable (`replicas > 1`), it **cannot** co-locate a `[[services]]` Postgres — point it at an external/managed database instead. A small dev/single-replica API may co-locate Postgres.
15
+
16
+ 4. **Isolate networks per app.** Give each app its own network scope; don't share a network across surfaces beyond what they must reach. The worker and API connect to the same database but are otherwise isolated.
17
+
18
+ 5. **Reference secret NAMES only.** List required secrets by NAME (DB URL, S3/files keys, API-key signing secret, webhook signing secret, SMTP for notifications). Never put values in the toml. Add a note that each is provisioned with `lwd secret set <NAME>` per environment.
19
+
20
+ 6. **Choose small vs. split topology.** **Small**: api + worker + co-located Postgres, single replicas — fine for dev/low volume. **Split**: replicated API (external DB) + independent worker + managed Postgres — for production scale. Pick based on the replica intent from step 1.
21
+
22
+ 7. **Generate, then review.** Run `forge generate lwd` (`--dry-run` first) to emit/update the manifests from `forge.json`. Hand-edit only for things the generator can't know: replica counts, external-DB endpoints, worker health specifics, network scoping. Re-run `forge doctor` to validate.
23
+
24
+ ## Output
25
+
26
+ The `deploy/*.lwd.toml` manifests (one per surface) with correct types, health checks, network isolation, and secret NAMES; the `forge generate lwd` command used (with `--dry-run`); a short topology recommendation (small vs. split) tied to replica intent; a list of secrets to `lwd secret set`; and an explicit note of any hand-edits made and why the generator couldn't produce them.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: obh-retrofit-files
3
+ description: Use when replacing ad-hoc upload/storage code (multer, Vercel Blob, local disk, raw S3 SDK) with @obh/files. Maps the existing storage choke-point to createFilesClient behind the same function signature, so the product handles only file_id and uploads go signed direct-to-storage.
4
+ ---
5
+
6
+ Purpose: consolidate all file storage behind the **files** primitive so the product never touches bytes or bucket credentials — it stores a `file_id` and asks `@obh/files` for signed URLs. Uploads go direct from client to storage; downloads are signed, time-boxed URLs.
7
+
8
+ ## Workflow
9
+
10
+ 1. **Locate storage code.** Grep for: `multer`, `formidable`, `@vercel/blob`, `fs.writeFile`/`createWriteStream` to an uploads dir, `@aws-sdk/client-s3` / `S3Client` / `getSignedUrl`, `putObject`, `Bucket`. Identify every choke-point where bytes enter or leave the app and how the resulting location is persisted (a URL column, a path, a bucket key).
11
+
12
+ 2. **Map the current model to file_id.** Wherever the product stores a URL/path/key, it should store a `file_id` (FK to the platform files table). List the columns and code paths that need to change from "store a URL" to "store an id, resolve to a signed URL on read".
13
+
14
+ 3. **Install the primitive.** Run `forge add files` (`--dry-run` first). This adds the files migration to `scripts/migrations.d/*` and the `@obh/files` client (`createFilesClient`/`pgAdapter`). Run `pnpm migrate`. Record required secret NAMES (bucket/endpoint/keys) for deploy — values go in via `lwd secret set`, never committed.
15
+
16
+ 4. **Swap behind the same signature.** Keep the existing function (e.g. `saveAvatar(userId, upload)`), but replace its body with `createFilesClient` calls: request a signed upload target, hand it to the client, and record the returned `file_id`. Callers don't change. Do the same for reads: a `getAvatarUrl(fileId)` that returns a signed URL.
17
+
18
+ 5. **Move to direct-to-storage uploads.** For large/user uploads, have the API mint a signed upload URL and return it; the client PUTs bytes straight to storage; the client then confirms the `file_id`. This removes multipart bodies and the app-as-proxy pattern. For server-generated files (reports, exports), upload from the worker and store the `file_id`.
19
+
20
+ 6. **Retire old storage.** Remove multer/blob/disk/S3 wiring and any static file-serving route. Plan a backfill: for existing rows, register current objects with `@obh/files` to obtain `file_id`s, then drop the old URL/path columns once nothing reads them.
21
+
22
+ ## Output
23
+
24
+ A **files retrofit plan**: a table of storage choke-point → replacement `@obh/files` call → the function signature kept stable → the column moving from URL/path to `file_id`; the `forge add files` + `pnpm migrate` steps; required secret NAMES; the direct-to-storage upload flow (mint URL → client PUT → confirm id); and a backfill/cutover note for existing rows.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: obh-retrofit-jobs
3
+ description: Use when moving slow, scheduled, or background work off the request path onto the OBH jobs queue. Finds cron routes, slow synchronous handlers, report/import/cleanup tasks and proposes converting each to an idempotent OBH Job enqueued inside a transaction, with a worker handler.
4
+ ---
5
+
6
+ Purpose: get long-running or deferred work out of API request handlers and into durable, retryable **jobs** (commands). Jobs are commands in `snake_case` (e.g. `send_invoice_email`, `rebuild_search_index`); they are enqueued transactionally and run in the worker.
7
+
8
+ ## Workflow
9
+
10
+ 1. **Find candidates.** Grep the API for: cron/scheduled routes (`/cron/*`, `node-cron`, `setInterval`, Vercel cron); handlers doing heavy synchronous work (loops over rows, external HTTP calls, PDF/report generation, bulk email); import/export processing; cleanup/retention tasks; anything that makes the request slow or can fail partway.
11
+
12
+ 2. **Name each as a command.** `snake_case` imperative: `generate_monthly_report`, `import_contacts_csv`, `purge_expired_sessions`, `send_welcome_email`. One job = one unit of retryable work.
13
+
14
+ 3. **Design an idempotent handler.** Each job must be safe to run more than once (at-least-once delivery). Use a natural idempotency key (resource id + operation, or a dedup column), upserts instead of blind inserts, and guard against double side-effects (e.g. mark-then-send). Define the payload as the minimal input the handler needs — pass ids, re-fetch current state inside the handler.
15
+
16
+ 4. **Place the enqueue inside a tx.** Enqueue the job in the **same transaction** as the state change that should trigger it, so a committed write always has its follow-up queued and a rolled-back write never leaks a job. Where a job should follow a fact, prefer emitting an event (see obh-add-events) and enqueueing the job from a subscriber — events are facts, jobs are the commands they trigger.
17
+
18
+ 5. **Install and scaffold.** Run `forge add jobs` (`--dry-run` first). This adds the queue migration in `scripts/migrations.d/*`, the `@obh/jobs` client (`createJobsClient`/`pgAdapter`/`runMigrations`), and a worker surface. Run `pnpm migrate`.
19
+
20
+ 6. **Write the worker handler.** Add a consumer under `apps/worker/src/consumers.d/*.ts` exporting `init(ctx)` / `tick(ctx)` (auto-loaded). Bind each `snake_case` job to its handler; keep handlers small, log start/finish, and let the queue handle retry/backoff rather than catching-and-swallowing.
21
+
22
+ 7. **Retire the old path.** Replace the synchronous body of the original route with an enqueue that returns quickly (202/accepted). Remove cron routes once the worker owns the schedule. Keep the old code behind the same function signature during transition if callers depend on it.
23
+
24
+ ## Output
25
+
26
+ A **jobs retrofit plan**: a table of source (route/task) → `job_name` (snake_case) → payload → idempotency key → enqueue tx site → worker consumer file; the `forge add jobs` + `pnpm migrate` steps; and per-route notes on what the handler returns after enqueue and which cron/synchronous code is removed.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: obh-sdk-extraction
3
+ description: Use when types and API-client code are duplicated between the backend and one or more frontends. Proposes extracting a single packages/sdk (shared types + one typed client), then migrating callers one resource at a time behind a compatibility re-export.
4
+ ---
5
+
6
+ Purpose: eliminate drift between backend and frontend(s) by making `packages/sdk` the single source of shared types and the one typed API client. The migration is incremental and non-breaking: callers move one resource at a time behind a compatibility re-export so nothing breaks mid-flight.
7
+
8
+ ## Workflow
9
+
10
+ 1. **Find the duplication.** Grep across `apps/api`, `apps/admin`, `apps/mobile`, and any frontend for: parallel type declarations (a `User`/`Invoice` interface defined in both backend and frontend), and hand-rolled/defensive fetch clients (bespoke `fetch` wrappers, per-call `try/catch` + manual JSON casting, ad-hoc base-URL handling). List each duplicated type and each client function, and which surfaces define/consume it.
11
+
12
+ 2. **Pick the source of truth.** For shared types, the backend's domain types are usually canonical. For the client, design one typed client whose methods mirror the API routes (`apps/api/src/routes/*`). Note where request/response shapes disagree between front and back — those mismatches are latent bugs to resolve during extraction.
13
+
14
+ 3. **Scaffold the package.** Run `forge add sdk` (`--dry-run` first) to create `packages/sdk` in the workspace and register it in `forge.json`. It holds shared types and a single `createClient`-style typed client — no framework-specific code, so every app (api, admin, mobile) can depend on it.
15
+
16
+ 4. **Move types first.** Relocate the canonical types into `packages/sdk`. In each place that previously declared them, replace the declaration with a **compatibility re-export** from the sdk (`export type { User } from '@obh/sdk'`) so existing imports keep resolving unchanged.
17
+
18
+ 5. **Migrate callers one resource at a time.** For each resource (users, invoices, …): point the frontend at the sdk client method, delete the hand-rolled client code for that resource, and remove its local types (now re-exported). Ship per-resource — the compatibility re-exports mean unmigrated resources keep working alongside migrated ones.
19
+
20
+ 6. **Retire the shims.** Once every caller imports from `@obh/sdk` directly, remove the compatibility re-exports and the last defensive client wrappers. Verify each frontend still type-checks against the sdk.
21
+
22
+ ## Output
23
+
24
+ An **sdk extraction plan**: a table of duplicated type / client-function → surfaces that define & consume it → canonical source → any front/back shape mismatch to fix; the `forge add sdk` step; a phased sequence (move types + re-export shims → migrate callers resource-by-resource → remove shims); and a per-resource checklist so the migration ships in safe, independent increments.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: obh-settings-migration
3
+ description: Use when consolidating scattered configuration — hardcoded constants, .env sprawl, or an untyped options/settings table — into typed @obh/settings definitions. Proposes scope/schema/default/sensitivity per key plus a read-through-write-both migration path.
4
+ ---
5
+
6
+ Purpose: replace ad-hoc config with the typed, scoped **settings** primitive. Each setting has a scope (global / workspace / user), a typed schema, a default, and a sensitivity flag. The migration is gradual: read through settings while writing both old and new stores until the old source is retired.
7
+
8
+ ## Workflow
9
+
10
+ 1. **Inventory current config.** Grep for three patterns: hardcoded constants in source (magic numbers, feature flags, limits, URLs); `.env` / `process.env.*` usage (env sprawl, especially non-secret tuning values); and untyped key/value config tables (`settings`, `options`, `preferences`, `config` with a `value text` column). Record each key, its current type, where it's read, and who it varies by.
11
+
12
+ 2. **Assign a scope.** Decide the narrowest scope that fits: **global** (one value for the whole deployment), **workspace/tenant** (per-organisation), or **user** (per-account). A single hardcoded constant that never varies is global; anything read per-request off the tenant/user is scoped.
13
+
14
+ 3. **Define a typed schema + default.** For each key give a concrete type (boolean, number with range, enum, string, structured object) and a sensible default so a missing value never breaks. Group related keys under one definition where they move together.
15
+
16
+ 4. **Mark sensitivity.** Flag secrets/credentials as sensitive — those stay out of logs and audit payloads and are supplied via deploy secret NAMES, not stored as plain settings values. Non-secret tuning values are normal settings and can move out of `.env` entirely.
17
+
18
+ 5. **Install the primitive.** Run `forge add settings` (`--dry-run` first). This adds the settings migration to `scripts/migrations.d/*` and the `@obh/settings` client (`defineX`/`createSettingsClient`/`pgAdapter`). Run `pnpm migrate`. Write the `defineSettings` schemas from step 3.
19
+
20
+ 6. **Migrate read-through, write-both.** Introduce a resolver that reads from `@obh/settings` and falls back to the old source (constant/env/options table) when unset — this is safe to ship immediately. For writable config, write to both stores during transition. Backfill existing option-table rows into settings. Once reads are served entirely from settings and backfill is verified, remove the fallback and the old constants/env keys/table.
21
+
22
+ ## Output
23
+
24
+ A **settings migration plan**: a table of key → scope → typed schema → default → sensitivity → current source (constant/env/table) and read sites; the `forge add settings` + `pnpm migrate` steps with the `defineSettings` definitions to write; and a phased cutover (read-through-with-fallback → write-both + backfill → remove old source), calling out which keys are sensitive and stay as deploy secrets.