create-flowdular 0.5.1 → 0.6.1

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 (105) hide show
  1. package/README.md +8 -5
  2. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
  4. package/agent-template/.agents/skills/database-adapter/SKILL.md +5 -5
  5. package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  6. package/agent-template/.agents/skills/deploy-operate/SKILL.md +1 -1
  7. package/agent-template/.agents/skills/migration-authoring/SKILL.md +4 -4
  8. package/agent-template/.agents/skills/module-new/SKILL.md +1 -1
  9. package/agent-template/.agents/skills/spec-approval/SKILL.md +6 -2
  10. package/agent-template/.agents/skills/spec-interview/SKILL.md +2 -2
  11. package/agent-template/.agents/skills/test-hardening/SKILL.md +2 -2
  12. package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
  13. package/agent-template/.ai/agents/sandbox/business-manager.md +2 -2
  14. package/agent-template/.ai/blueprints/add-migration/required-files.yaml +1 -1
  15. package/agent-template/.ai/blueprints/new-module/required-files.yaml +1 -1
  16. package/agent-template/.ai/platform-capabilities.md +10 -8
  17. package/agent-template/.ai/policies/capabilities.yaml +28 -12
  18. package/agent-template/.ai/skills/README.md +1 -1
  19. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -1
  20. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  21. package/agent-template/.ai/skills/database-adapter/SKILL.md +5 -5
  22. package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  23. package/agent-template/.ai/skills/deploy-operate/SKILL.md +1 -1
  24. package/agent-template/.ai/skills/migration-authoring/SKILL.md +4 -4
  25. package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
  26. package/agent-template/.ai/skills/spec-approval/SKILL.md +6 -2
  27. package/agent-template/.ai/skills/spec-interview/SKILL.md +2 -2
  28. package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
  29. package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
  30. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  31. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  32. package/agent-template/.claude/skills/database-adapter/SKILL.md +5 -5
  33. package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  34. package/agent-template/.claude/skills/deploy-operate/SKILL.md +1 -1
  35. package/agent-template/.claude/skills/migration-authoring/SKILL.md +4 -4
  36. package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
  37. package/agent-template/.claude/skills/spec-approval/SKILL.md +6 -2
  38. package/agent-template/.claude/skills/spec-interview/SKILL.md +2 -2
  39. package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
  40. package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
  41. package/agent-template/docs/adr/0003-module-settings.md +2 -0
  42. package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
  43. package/agent-template/docs/agent-contract.md +3 -3
  44. package/agent-template/docs/cli-extensions.md +1 -0
  45. package/agent-template/docs/cli.md +40 -3
  46. package/agent-template/docs/configuration.md +64 -9
  47. package/agent-template/docs/database-adapters.md +30 -22
  48. package/agent-template/docs/design-system.md +7 -3
  49. package/agent-template/docs/getting-started.md +29 -32
  50. package/agent-template/docs/module-distribution.md +79 -86
  51. package/agent-template/docs/module-web-surfaces.md +9 -7
  52. package/agent-template/docs/modules.md +6 -2
  53. package/agent-template/docs/sandbox.md +23 -4
  54. package/agent-template/platform/scripts/build.mjs +7 -0
  55. package/dist/bin.js +3 -6
  56. package/package.json +1 -1
  57. package/template/default/.env.example +8 -3
  58. package/template/default/.vercelignore +8 -0
  59. package/template/default/README.md +20 -11
  60. package/template/default/_gitignore +3 -2
  61. package/template/default/infra/README.md +86 -65
  62. package/template/default/infra/docker/.env.example +71 -0
  63. package/template/default/infra/docker/Dockerfile +29 -11
  64. package/template/default/infra/docker/app-entrypoint.mjs +5 -0
  65. package/template/default/infra/docker/compose.yaml +109 -58
  66. package/template/default/infra/docker/database-urls.mjs +28 -0
  67. package/template/default/infra/docker/pitr.sh +177 -0
  68. package/template/default/infra/docker/postgres/10-roles.sh +16 -12
  69. package/template/default/infra/docker/start.mjs +402 -0
  70. package/template/default/infra/kubernetes/database-secret.example.yaml +3 -3
  71. package/template/default/infra/kubernetes/deployment.yaml +5 -0
  72. package/template/default/infra/sdk-module-manifests.mjs +118 -0
  73. package/template/default/infra/vercel/README.md +262 -0
  74. package/template/default/infra/vercel/build.mjs +223 -0
  75. package/template/default/infra/vercel/handler.mjs +100 -0
  76. package/template/default/modules/example/migrations/0001_example_core.up.sql +2 -2
  77. package/template/default/modules/example/module.json +1 -1
  78. package/template/default/modules/example/package.json +3 -3
  79. package/template/default/modules/example/spec/module.yaml +1 -1
  80. package/template/default/modules/example/src/services/migration.ts +2 -2
  81. package/template/default/modules/example/tests/module.test.ts +1 -1
  82. package/template/default/package.json +2 -3
  83. package/template/default/platform/octane.config.ts +290 -156
  84. package/template/default/platform/package.json +5 -5
  85. package/template/default/platform/scripts/build.mjs +56 -0
  86. package/template/default/platform/scripts/dev.mjs +101 -18
  87. package/template/default/platform/src/generated/modules.server.ts +1 -0
  88. package/template/default/platform/src/server/database.ts +24 -0
  89. package/template/default/platform/src/server/lifecycle.ts +325 -0
  90. package/template/default/platform/src/server/runtime-role.ts +33 -0
  91. package/template/default/platform/src/server/setup/access.ts +160 -0
  92. package/template/default/platform/src/server/setup/adapters.ts +554 -0
  93. package/template/default/platform/src/server/setup/environment.ts +154 -0
  94. package/template/default/platform/src/server/setup/gate.ts +84 -0
  95. package/template/default/platform/src/server/setup/index.ts +181 -0
  96. package/template/default/platform/src/server/setup/modules.ts +119 -0
  97. package/template/default/platform/src/server/setup/page.ts +548 -0
  98. package/template/default/platform/src/server/setup/routes.ts +788 -0
  99. package/template/default/platform/src/server/setup/sanitize.ts +111 -0
  100. package/template/default/platform/src/server/setup/seed.ts +192 -0
  101. package/template/default/platform/src/server/setup/token.ts +79 -0
  102. package/template/default/platform/src/server/worker-tick.ts +193 -0
  103. package/template/default/platform/src/server/workspace-root.ts +16 -0
  104. package/template/default/render.yaml +70 -0
  105. package/template/default/vercel.json +5 -0
@@ -9,23 +9,38 @@ default: deny
9
9
 
10
10
  # packages/cli/src/capabilities.ts
11
11
  core:
12
+ deploy.start.local:
13
+ command: pnpm flowdular deploy start docker --apply
14
+ risk: process
15
+ supportsDryRun: true
16
+ effect: runs the existing Docker Compose launcher in the operator's terminal after checking its files and Docker Compose; the launcher creates local secrets on first start and opens setup
17
+ deploy.start.vercel:
18
+ command: pnpm flowdular deploy start vercel --apply
19
+ risk: process
20
+ supportsDryRun: true
21
+ effect: drives the signed-in Vercel CLI in the operator's terminal to link the project, provision Neon PostgreSQL and its runtime and background roles, upload the stable keys from a 0600 local backup through stdin, connect a private Blob store, deploy to Production and print a one-time setup token whose SHA-256 alone is stored in Vercel
22
+ module.source:
23
+ command: pnpm flowdular module source add <name> <location> [--apply]
24
+ risk: workspace-write
25
+ supportsDryRun: true
26
+ note: host-only source configuration; sandbox agents receive no network or Git access
27
+ module.plan:
28
+ command: pnpm flowdular module plan <id[@version]> --source <name> [--apply]
29
+ risk: workspace-write
30
+ supportsDryRun: true
31
+ note: saves exact source, release digests and impact without executing module code
32
+ module.apply:
33
+ command: pnpm flowdular module apply <plan-id> [--apply]
34
+ risk: workspace-write
35
+ supportsDryRun: true
36
+ note: host-only source installation; activation and database changes remain separate
12
37
  module.search:
13
38
  command: pnpm flowdular module search [query]
14
39
  risk: read
15
- note: host-only official HTTPS catalog lookup; does not grant sandbox network access
40
+ note: host-only configured source lookup; does not grant sandbox network access
16
41
  module.info:
17
42
  command: pnpm flowdular module info <id>
18
43
  risk: read
19
- module.install:
20
- command: pnpm flowdular module install <id[@version]> [--apply]
21
- risk: workspace-write
22
- supportsDryRun: true
23
- note: host-only source installation; no scripts, activation, permissions or database changes
24
- module.update:
25
- command: pnpm flowdular module update <id[@version]> [--apply]
26
- risk: workspace-write
27
- supportsDryRun: true
28
- note: rejects local edits, changed historical migrations and downgrades
29
44
  module.recover:
30
45
  command: pnpm flowdular module recover [--apply]
31
46
  risk: workspace-write
@@ -71,7 +86,7 @@ core:
71
86
  risk: process
72
87
  localOnly: true
73
88
  supportsDryRun: true
74
- effect: applies or adopts the outstanding migrations of one module against the configured PostgreSQL (embedded PGlite outside production) and writes the _coreloom_migrations_v2 ledger; without --apply it reports the plan and writes nothing
89
+ effect: applies or adopts the outstanding migrations of one module against the configured PostgreSQL (embedded PGlite outside production) and writes the _flowdular_migrations_v2 ledger; without --apply it reports the plan and writes nothing
75
90
  database.backup:
76
91
  command: pnpm flowdular database backup --output <dir> [--apply]
77
92
  risk: process
@@ -109,6 +124,7 @@ commandsWithoutDescriptor:
109
124
  - pnpm flowdular module enable <id> [--apply] (with --apply also runs auth.scopes.sync; result field scopes, failure code MODULE_SCOPES_SYNC_FAILED)
110
125
  - pnpm flowdular module disable <id> [--apply]
111
126
  - pnpm flowdular setup check
127
+ - pnpm flowdular deploy targets|plan <docker|kubernetes|render|vercel|cloudflare>
112
128
  - pnpm flowdular setup quick (alias of auth.greenfield.reset)
113
129
 
114
130
  # modules/*/src/cli/commands.json, loaded only for modules enabled in flowdular.json
@@ -23,7 +23,7 @@ Flowdular is an agentic foundation framework: the platform is the foundation, an
23
23
  | `business-agent-design` | Ship a module-owned business agent with an exact tool ceiling, tenant binding, revisions, and access tests. |
24
24
  | `integration-adapter` | Add a source or sink adapter for a named service: connector, port, mapping, recorded fixture, consent, call log. |
25
25
  | `variables` | Variable-aware fields and templates: the `{{ }}` contract, the scope mask, server-side resolution, adding a source. |
26
- | `workflow-development` | Build, publish, invoke, simulate, and test typed durable workflows and their module integration capability. |
26
+ | `workflow-development` | Build, publish, invoke, simulate, and test typed durable workflows, including module-owned custom node templates. |
27
27
  | `release-eject-pr` | Sandbox eject sequence, repository gates, branch and PR conventions, post-merge scope grant. |
28
28
  | `deploy-operate` | Container build, production env keys, migrations at rollout, health and readiness, backup and restore, rollback. |
29
29
 
@@ -24,7 +24,7 @@ also ship a ready business agent through `defineAgent()`, read
24
24
  ## 1. The contract in code
25
25
 
26
26
  - Composition: `PlatformServerContext` (`modules/auth/src/server/composition.ts`) carries `agentTools: PlatformToolRegistry` (`register(tools)`, `list()`; `packages/kernel/src/tool-registry.ts`; a duplicate tool id throws at boot), `settings: ModuleSettingsRuntime`, and `capabilities: PlatformCapabilityRegistry` (`register(id, service)`, `get(id)`, `has(id)`; `packages/kernel/src/capability-registry.ts`). `platform/octane.config.ts` creates the registries, passes them to every module's `createServerComposition`, declares each `settings`, owns each `dispose`, then calls each `start`.
27
- - Ordering is a non-issue: `agents.core` (`modules/agents/src/platform.ts`) passes `tools: () => context.agentTools.list()` into `createAgentRuntime`, and the harness is built lazily in `start()`, which runs after every module has composed. Tools any module registers during its own compose are therefore visible, whatever the module order.
27
+ - Ordering is a non-issue: `agents.core` (`modules/agents/src/platform.ts`) passes `tools: () => context.agentTools.list()` into `createAgentRuntime`, and the harness is built when `agents.core` opens, on its first request, capability call or `startWorker()`, all of which run after every module has composed. Tools any module registers during its own compose are therefore visible, whatever the module order.
28
28
  - Helpers: import `defineApiAgentTool` from `@flowdular/sdk/harness/tool-adapters` and the types `AgentTool`, `AgentToolContext` from `@flowdular/sdk/harness/runtime`. Both subpaths are free of the Vercel AI SDK; only the harness root (`@flowdular/sdk/harness`) and `@flowdular/sdk/modules/agents/server` pull it. `defineApiAgentTool` returns a frozen `AgentTool { id, transport: 'api', target, description, requiredPermissions, inputSchema?, execute }`. `defineCliAgentTool({ id, capability: { id, risk }, ... })` wraps a CLI capability and throws at definition time for `external` or `destructive` risk.
29
29
  - Skills inside `agents.core` are tenant database records behind `agents.skills.*`, appended to agent instructions. They are unrelated to `.ai/skills/**`, which are files for coding agents.
30
30
  - A read tool's output can also become a resolvable `{{ variable }}` for variable-aware fields: the tool's `requiredPermissions` is the variable's scope mask. Register a source on `platformVariableRegistry(context.capabilities)`, require an explicit record binding, and invoke the tool with the trusted tenant, actor permission snapshot, and signal. See the `variables` skill for the complete refusal contract.
@@ -63,7 +63,7 @@ Recipe in `modules/auth/tests/endpoints.test.ts`: build the runtime with a `Data
63
63
 
64
64
  - 401 without a cookie or token.
65
65
  - 403 with a principal that lacks the permission.
66
- - Cross-tenant read returns an empty list (service level, on the suite's test provider under the non-bypass `coreloom_runtime` role).
66
+ - Cross-tenant read returns an empty list (service level, on the suite's test provider under the non-bypass `flowdular_runtime` role).
67
67
  - Mutation without `x-csrf-token` returns 403 `CSRF_REJECTED`; without `origin` returns 403.
68
68
  - Each validation bound returns 400 with its code.
69
69
 
@@ -94,12 +94,12 @@ PostgreSQL tenant-owned tables also use database-enforced isolation:
94
94
 
95
95
  - enable and force row-level security on the table;
96
96
  - define a policy whose `USING` and `WITH CHECK` clauses compare `tenant_id`
97
- with `current_setting('coreloom.tenant_id', true)`;
97
+ with `current_setting('flowdular.tenant_id', true)`;
98
98
  - run application traffic under a role that is neither a superuser nor granted
99
99
  `BYPASSRLS`;
100
100
  - use a separate migration role for DDL or policy ownership when required.
101
101
 
102
- The adapter sets `coreloom.tenant_id` with parameterized `set_config(..., true)`
102
+ The adapter sets `flowdular.tenant_id` with parameterized `set_config(..., true)`
103
103
  after `BEGIN` on the pinned connection. Never use an unpinned root query.
104
104
  Explicit tenant predicates remain required as defense in depth.
105
105
 
@@ -139,7 +139,7 @@ transactions without `tenantId`, fail with `TENANT_CONTEXT_REQUIRED`.
139
139
 
140
140
  Use `DatabaseMigration` and `runDatabaseMigrations` from `@flowdular/sdk/database`.
141
141
  Each migration has one immutable id and its PostgreSQL SQL. The ledger is
142
- `_coreloom_migrations_v2`, keyed by module namespace and migration id; its
142
+ `_flowdular_migrations_v2`, keyed by module namespace and migration id; its
143
143
  checksum covers that exact SQL.
144
144
 
145
145
  `inspectExisting(database)` is the only pre-ledger adoption proof. Use
@@ -163,8 +163,8 @@ constant. A migration-only task uses `migration-authoring` in a separate phase.
163
163
  ## 7. Tests run on a real PostgreSQL
164
164
 
165
165
  `createTestDatabaseProvider()` from `@flowdular/sdk/database-testing` gives a suite its
166
- own PostgreSQL in process by default, with the same `coreloom_runtime` and
167
- `coreloom_background` roles and the same forced row-level security a deployment
166
+ own PostgreSQL in process by default, with the same `flowdular_runtime` and
167
+ `flowdular_background` roles and the same forced row-level security a deployment
168
168
  enforces. There is no server to start and no second dialect to keep green, so
169
169
  the isolation assertions run on every turn rather than behind an environment
170
170
  flag. CI selects server PostgreSQL with `FD_TEST_DATABASE_ADAPTER=postgresql`
@@ -83,8 +83,8 @@ adapter change, sandbox eject, and deployment validation.
83
83
 
84
84
  Every turn runs against a real PostgreSQL, because the embedded one starts in
85
85
  process. The sandbox and the test suites use `createTestDatabaseProvider()` from
86
- `@flowdular/sdk/database-testing`, which brings the `coreloom_runtime` and
87
- `coreloom_background` roles and forced row-level security with it.
86
+ `@flowdular/sdk/database-testing`, which brings the `flowdular_runtime` and
87
+ `flowdular_background` roles and forced row-level security with it.
88
88
 
89
89
  A target run covers tenant A and B fixtures, operations without tenant context,
90
90
  forged cross-tenant inserts, direct row-security bypass probes, concurrent
@@ -53,7 +53,7 @@ Set `FD_TRUST_PROXY` behind a load balancer. `FD_DATABASE_BACKGROUND_URL` gives
53
53
 
54
54
  ## 3. Migrations at rollout
55
55
 
56
- Migrations are module-owned, numbered, immutable once applied, and verified by checksum against the `_coreloom_migrations_v2` ledger. The commands (`packages/cli/src/runner.ts`):
56
+ Migrations are module-owned, numbered, immutable once applied, and verified by checksum against the `_flowdular_migrations_v2` ledger. The commands (`packages/cli/src/runner.ts`):
57
57
 
58
58
  ```bash
59
59
  pnpm flowdular migration status [--module <id>] # what the ledger holds
@@ -32,7 +32,7 @@ Migration SQL is checked in and immutable after release:
32
32
  ## 2. What the v2 runner guarantees
33
33
 
34
34
  `runDatabaseMigrations(database, namespace, databaseMigrations)` uses the
35
- namespaced `_coreloom_migrations_v2` ledger. A row records namespace, migration
35
+ namespaced `_flowdular_migrations_v2` ledger. A row records namespace, migration
36
36
  id, dialect id, checksum, and applied time. The checksum covers the selected
37
37
  dialect's exact SQL.
38
38
 
@@ -82,14 +82,14 @@ Every PostgreSQL tenant table includes:
82
82
  ALTER TABLE inventory_locations ENABLE ROW LEVEL SECURITY;
83
83
  ALTER TABLE inventory_locations FORCE ROW LEVEL SECURITY;
84
84
  CREATE POLICY inventory_locations_tenant_policy ON inventory_locations
85
- USING (tenant_id = current_setting('coreloom.tenant_id', true))
86
- WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
85
+ USING (tenant_id = current_setting('flowdular.tenant_id', true))
86
+ WITH CHECK (tenant_id = current_setting('flowdular.tenant_id', true));
87
87
  ```
88
88
 
89
89
  The runtime role is not a superuser and has no `BYPASSRLS`. DDL and policy
90
90
  ownership use `purpose: 'migration'`. Runtime repository calls use
91
91
  `database.transaction(operation, { tenantId, access })`; the adapter sets
92
- transaction-local `coreloom.tenant_id` on the pinned connection. Queries still
92
+ transaction-local `flowdular.tenant_id` on the pinned connection. Queries still
93
93
  include `WHERE tenant_id = ...` as defense in depth.
94
94
 
95
95
  ## 6. Add one migration
@@ -148,7 +148,7 @@ export function createServerComposition(
148
148
 
149
149
  `PlatformServerContext` also carries `databases: DatabaseProvider`, `settings: ModuleSettingsRuntime`, `agentTools: PlatformToolRegistry`, `agentDefinitions: PlatformAgentRegistry`, and `capabilities: PlatformCapabilityRegistry`. The runtime shares one lazy database initialization, uses separate migration and runtime leases, and releases the runtime lease from `dispose()`; `prepare()` remains read-only. A composition may return module settings, lifecycle hooks, agent registrations, and typed cross-module capabilities as described in the focused skills.
150
150
 
151
- Schema: write PostgreSQL SQL in `migrations/0001_inventory_core.up.sql` and mirror it byte for byte in `databaseMigrations` as `sql: { postgresql: ... }` with an `inspectExisting` built from `postgresTenantTableState(...)`. Tenant tables enable and force row-level security with an `<table>_tenant_policy` whose `USING` and `WITH CHECK` compare `tenant_id` with `current_setting('coreloom.tenant_id', true)`; the runtime role has no superuser or `BYPASSRLS`. Repository operations use `database.transaction(..., { tenantId, access })` and retain explicit tenant predicates. Details in `migration-authoring` and `database-adapter`.
151
+ Schema: write PostgreSQL SQL in `migrations/0001_inventory_core.up.sql` and mirror it byte for byte in `databaseMigrations` as `sql: { postgresql: ... }` with an `inspectExisting` built from `postgresTenantTableState(...)`. Tenant tables enable and force row-level security with an `<table>_tenant_policy` whose `USING` and `WITH CHECK` compare `tenant_id` with `current_setting('flowdular.tenant_id', true)`; the runtime role has no superuser or `BYPASSRLS`. Repository operations use `database.transaction(..., { tenantId, access })` and retain explicit tenant predicates. Details in `migration-authoring` and `database-adapter`.
152
152
 
153
153
  Errors: `{ error: { code, message } }`; service errors `class XServiceError extends Error { constructor(readonly code: string, message: string, readonly status = 400) }`; a `failure(error)` helper routes them to `jsonResponse(..., error.status)` and everything else to `problemResponse(error, 'The <module> operation failed.')`.
154
154
 
@@ -51,8 +51,12 @@ is a new spec-authoring step and needs approval after that edit.
51
51
  ## 3. Sandbox path
52
52
 
53
53
  In the sandbox, use the operator approval action for the selected session module.
54
- The live route is POST /sandbox/api/sessions/:id/approve, exposed by
55
- approveSpecification in packages/sandbox/src/client/api.ts.
54
+ The live route is POST /sandbox/api/sessions/:id/approve with
55
+ { "module", "specHash" }, exposed by approveSpecification in
56
+ packages/sandbox/src/client/api.ts. specHash is the SHA-256 the review card
57
+ shows for the text it renders. The route refuses with 409 SPEC_CHANGED when the
58
+ current text has another hash, and with 409 QUESTIONS_PENDING while the module
59
+ has unanswered questions; it records nothing then.
56
60
 
57
61
  The route changes the status presentation and records the SHA-256 hash of the
58
62
  exact approved text in the session. Do not patch the session workspace file to
@@ -74,7 +74,7 @@ In the sandbox, end the reply with exactly one fenced block tagged `questions`,
74
74
  ```
75
75
  ````
76
76
 
77
- The sandbox renders it as a form and the answers return in the next turn as a `Decisions` section. Outside the sandbox: in Claude Code ask through the question tool with the same options, and in Codex ask in plain text with the options numbered. In every host, `recommended` is the default from the card, and an unanswered question stays a question, never a guess.
77
+ The sandbox enforces the bounds: at most 12 questions, each 1 to 400 characters; at most 8 options per question, each 1 to 120 characters; `recommended` is one of the options; no line breaks inside a value; the whole block at most 8000 characters. A block outside them comes back to you once with the reason. The sandbox renders it as a form and the answers return in the next turn as a `Decisions` section. Outside the sandbox: in Claude Code ask through the question tool with the same options, and in Codex ask in plain text with the options numbered. In every host, `recommended` is the default from the card, and an unanswered question stays a question, never a guess.
78
78
 
79
79
  When the answers come back, copy each one into `decisions[]` with `decidedBy: user` and the answer text, and update whatever the answer changed.
80
80
 
@@ -92,7 +92,7 @@ A number the case needs (a score, a premium, a price per square metre) is an `ac
92
92
 
93
93
  `modules/<dir>/spec/module.yaml`, `schemaVersion: 2`, `status: draft`. Keep the v1 keys (`id`, `specVersion`, `name`, `description`, `profile`, `capabilities`, `dependencies`, `tenancy`, `locales`, `invariants`, `permissions`, `dataOwnership`, `acceptanceScenarios`) and add the v2 arrays:
94
94
 
95
- - `entities[]`: `{ id, name, fields[], states? }`. A field is `{ id, type, required?, unique?, maxLength?, values?, reference?, description? }`. `type` is one of `string`, `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `enum`, `reference`, `json`; `unique` is `tenant` or `none`. `enum` needs `values`, `reference` needs `reference`. Entity and screen ids are `^[a-z][a-z0-9-]*$`; field and setting keys are `^[a-z][a-zA-Z0-9]*$`. Money is `integer` minor units plus an explicit currency field, never `decimal`.
95
+ - `entities[]`: `{ id, name, fields[], states? }`. A field is `{ id, type, required?, unique?, maxLength?, values?, reference?, description? }`. `type` is one of `string`, `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `enum`, `reference`, `json`; `unique` is `tenant` or `none`. `enum` needs `values`, `reference` needs `reference`. Entity and screen ids are `^[a-z][a-z0-9-]*$`; field and setting keys are `^[a-z][a-zA-Z0-9]*$`. A field never repeats a column every tenant table owns (`id`, `tenantId`, `createdAt`; a screen may still list `createdAt`), and its key in snake case is never a PostgreSQL reserved word (`order`, `user`, `currentUser`): `spec-schema` refuses either with `SPEC_FIELD_RESERVED`. Money is `integer` minor units plus an explicit currency field, never `decimal`.
96
96
  - `screens[]`: `{ id, kind: list|record|form|dashboard, entity?, title?, columns?, filters?, navigationGroup? }`. `navigationGroup` is one of the six values on the card.
97
97
  - `actions[]`: `{ id, entity?, permission, kind: create|update|delete|custom, risk, idempotent, description }`. `risk: external` is refused by the platform, so an action may not declare it.
98
98
  - `widgets[]`: `{ id, slot, entity?, description }`; `slot` is one of the four workspace slots.
@@ -23,7 +23,7 @@ when: A module has few or tautological tests, a bug escaped the suite, or a revi
23
23
 
24
24
  ## 2. Repositories on an embedded PostgreSQL
25
25
 
26
- `createPgliteTestProvider()` from `@flowdular/sdk/database-testing` runs a real PostgreSQL inside the test process, with the same `coreloom_runtime` and `coreloom_background` roles and the same forced row-level security a deployment enforces. Booting it costs about two seconds, so a suite opens one provider per test file, migrates it once, and truncates the module's tables between cases; `.ai/references/catalog/tests/support/database.ts` is the shape (`createCatalogTestDatabase` hands out a lease per fixture, `closeCatalogTestDatabases` runs in `afterAll`). Build the service on top: `new CatalogService((await createCatalogTestDatabase()).repository)`. A database module also keeps `tests/migrations.test.ts` for SQL byte parity, fresh apply, safe pre-ledger adoption, and a clean second start. A module whose spec has no `database` capability gets a `MemoryXRepository` from the scaffold instead; a module with a database tests the database repository, never a hand-written fake, because the SQL, the ledger and the row-level security are what need testing.
26
+ `createPgliteTestProvider()` from `@flowdular/sdk/database-testing` runs a real PostgreSQL inside the test process, with the same `flowdular_runtime` and `flowdular_background` roles and the same forced row-level security a deployment enforces. Booting it costs about two seconds, so a suite opens one provider per test file, migrates it once, and truncates the module's tables between cases; `.ai/references/catalog/tests/support/database.ts` is the shape (`createCatalogTestDatabase` hands out a lease per fixture, `closeCatalogTestDatabases` runs in `afterAll`). Build the service on top: `new CatalogService((await createCatalogTestDatabase()).repository)`. A database module also keeps `tests/migrations.test.ts` for SQL byte parity, fresh apply, safe pre-ledger adoption, and a clean second start. A module whose spec has no `database` capability gets a `MemoryXRepository` from the scaffold instead; a module with a database tests the database repository, never a hand-written fake, because the SQL, the ledger and the row-level security are what need testing.
27
27
 
28
28
  ## 3. Route recipe (from `modules/auth/tests/endpoints.test.ts`)
29
29
 
@@ -63,7 +63,7 @@ The auth middleware must have set the principal for `endpointIdentityFromContext
63
63
  - 403 on a mutation without `x-csrf-token` (`CSRF_REJECTED`) and without `origin` (`ORIGIN_REQUIRED`).
64
64
  - 400 with the stable code for each validation bound (`INVALID_INPUT`, module codes such as `INVALID_ITEM_KIND`).
65
65
  - 409 for the tenant-scoped uniqueness rule, and success for the same key in another tenant.
66
- - Tenant isolation: rows created for `tenant-a` are invisible to `list('tenant-b')`. The provider hands the suite the non-bypass `coreloom_runtime` role, so this runs against real forced row-level security; also assert that a call without tenant context fails with `TENANT_CONTEXT_REQUIRED`.
66
+ - Tenant isolation: rows created for `tenant-a` are invisible to `list('tenant-b')`. The provider hands the suite the non-bypass `flowdular_runtime` role, so this runs against real forced row-level security; also assert that a call without tenant context fails with `TENANT_CONTEXT_REQUIRED`.
67
67
  - Identity: `moduleDefinition.manifest.id` equals the module id (keeps `module.json` and `src/index.ts` aligned). The scaffold writes this and the isolation case; everything else in this list is yours.
68
68
 
69
69
  Assert at the observation boundary: status code, `error.code`, returned record fields. Do not assert internal helper names, call order, or SQL text.
@@ -1,14 +1,13 @@
1
1
  ---
2
2
  name: workflow-development
3
3
  description: >-
4
- Build, publish, invoke, and test a workflows.core DAG through its typed graph
5
- and public execution capability without bypassing agent, action, tenant, or
6
- audit boundaries.
4
+ Author module-owned action templates, or build, publish, invoke, and test a
5
+ workflows.core DAG through its typed graph and public execution capability.
7
6
  roles:
8
7
  - agentic-engineer
9
8
  - frontend-engineer
10
9
  - module-executor
11
- when: A brief asks for a workflow, pipeline, canvas node, workflow action, or a module feature that starts a workflow.
10
+ when: A brief asks for a workflow, pipeline, canvas node, action-backed workflow template, or a module feature that starts a workflow.
12
11
  ---
13
12
 
14
13
  # Build and integrate an agentic workflow
@@ -18,18 +17,22 @@ pinned agent revisions, deterministic gates, schema validators, registered
18
17
  module actions, data mappings, and terminal output. It does not own schedules or
19
18
  webhook secrets. Those remain optional concerns of `automations.core`.
20
19
 
21
- Read `docs/adr/0006-agentic-workflows.md`, the approved
22
- `modules/workflows/spec/module.yaml`, and the contracts in
23
- `modules/workflows/src/domain/types.ts` before changing a workflow surface.
20
+ At the repository root, read `docs/adr/0006-agentic-workflows.md`, the approved
21
+ `modules/workflows/spec/module.yaml`, and the owning public contracts before
22
+ changing a workflow surface. In the Sandbox, read the approved active-module
23
+ specification and the relevant public contract under `reference/sdk` when it is
24
+ installed. A missing public contract is a core blocker, not a reason to invent
25
+ one or search beyond the session workspace.
24
26
 
25
27
  ## Pick the correct extension point
26
28
 
27
29
  - A workflow definition belongs in `workflows.core` and is edited through its
28
30
  API or canvas. Do not hardcode a tenant workflow in source.
29
31
  - A business operation that a workflow may call is a versioned agent action.
30
- Register it through the agents action catalog. If missing, implement it in a
31
- separate `agent-tool-design` phase with permission, input, output, timeout,
32
- idempotency and audit tests before returning to workflow integration.
32
+ Register one ordinary `AgentTool` with `context.agentTools`. Optional
33
+ `workflowTemplate` metadata makes that action a named palette choice through
34
+ `agents.actions.v2`; it creates no second handler or node kind. Use the
35
+ authoring recipe below when the action is part of this task.
33
36
  - A business module that starts a workflow resolves
34
37
  `workflows.execution.v1` from `context.capabilities`. It never imports a
35
38
  workflow repository or database.
@@ -39,6 +42,87 @@ Read `docs/adr/0006-agentic-workflows.md`, the approved
39
42
  - If the workflow module is absent, the capability registry returns `null`.
40
43
  Hide an optional feature or return a clear stable refusal.
41
44
 
45
+ ## Author a module-owned workflow node template
46
+
47
+ An action-backed template is module source, not graph source. The business
48
+ manager first records its stable action id, permission, named inputs and
49
+ validation rules, output, effect, replay behavior, and success and refusal
50
+ scenarios in the owning module's specification. In a Sandbox session, the
51
+ operator must approve the hash of that exact spec before implementation. A
52
+ later edit requires renewed approval. If any of these decisions are absent,
53
+ hand the spec delta to `business-manager`; do not infer a field, permission,
54
+ external effect, or idempotency rule from the brief.
55
+
56
+ Work inside the existing role boundaries:
57
+
58
+ 1. `backend-engineer` owns the tenant-bound service, code-level validator,
59
+ endpoint or CLI target, and durable target ledger for a mutation. Validation
60
+ runs before any write and returns a stable bounded refusal. A repeated
61
+ idempotency key returns its first result; the same key with different input
62
+ conflicts. Ask backend to supply a missing service rather than writing in
63
+ `src/services/**` or `src/api/**` from the agentic role.
64
+ 2. `agentic-engineer` defines the tool under `src/agent/**`, registers it once
65
+ from `createServerComposition` in `src/platform.ts`, and adds behavioral
66
+ tests. `defineApiAgentTool` or `defineCliAgentTool` carries the existing
67
+ public target. `agents.core` exposes the registered action through
68
+ `agents.actions.v2`; the business module does not register that capability
69
+ or access the workflow database.
70
+ 3. Give the tool a stable dotted id, positive `contractVersion`, required
71
+ permission, bounded input and output JSON Schemas, `risk: 'read'` or
72
+ `'workspace-write'`, `idempotency: 'required'`, cancellation policy, and
73
+ timeout. A workspace write also needs
74
+ `idempotencyProtection: 'target-ledger'` backed by the service's real ledger.
75
+ The handler receives trusted tenant, actor, permission snapshot,
76
+ idempotency key, and `AbortSignal` from `AgentToolContext`; none comes from
77
+ graph input. Its output must satisfy its declared schema.
78
+ 4. Add static `workflowTemplate: { label, description, effect }` on that same
79
+ tool. The label is at most 80 characters, the description at most 240, and
80
+ effect is `local` or `connector-egress`. Metadata contains no code,
81
+ credential, or tenant value. Invalid or duplicate metadata must fail
82
+ composition with a safe diagnostic. A workflow-eligible tool without the
83
+ metadata remains in the generic action editor.
84
+
85
+ The selected template becomes a normal `action` node with input, success, and
86
+ failure ports. It pins the action id, contract version, schemas, permissions,
87
+ risk, idempotency protection, timeout, cancellation, and effect. Changing any
88
+ of those or the handler's behavior requires a new action identity or contract
89
+ version. The current registry keeps one version per action id, so retain the
90
+ old id and register a distinct id when published graphs must keep running;
91
+ label and description may change without rebinding a published graph.
92
+
93
+ Graph bindings may never supply a raw secret. `writeOnly` and
94
+ `x-flowdular-secret` apply at every schema depth, including array items and
95
+ alternatives. A marked field cannot carry `default`, `const`, `enum`,
96
+ `example`, or `examples` data. A template requiring
97
+ a raw secret input is ineligible. Accept a nonsecret opaque reference and let
98
+ the owning module resolve a credential from its tenant-bound vault or a
99
+ declared capability. Never put secret values in fixtures, graph definitions,
100
+ events, audit, or error text.
101
+
102
+ For `connector-egress`, the consumer module declares `connectors.core` and
103
+ `connectors.calls.v1`, uses `caller: 'workflow'`, checks the instance's
104
+ `allowWorkflows` consent, and passes the stable workflow side-effect key to
105
+ the connector. Credentials remain in `connectors.core`. A replay-stable
106
+ output may contain the recorded call id and outcome, not a response body the
107
+ connector replay does not return. Propagate `CALL_OUTCOME_UNKNOWN` as a
108
+ terminal failure when the remote mutation may have happened but no call was
109
+ recorded; never send a second request just to rebuild output. A separate
110
+ provider contract with remote idempotency or readback is required before
111
+ retrying an uncertain mutation.
112
+
113
+ Prove the module behavior at its public service or action boundary: valid
114
+ input, structural and business validation refusal before mutation, missing
115
+ permission, foreign tenant, same-key replay, different-input conflict,
116
+ cancellation, and recovery around the target commit. Connector actions also
117
+ prove absent consent and `CALL_OUTCOME_UNKNOWN` with a recorded fixture. Run a
118
+ deterministic workflow simulation on invented, reviewed fixtures for success,
119
+ failure, and refusal edges. Assert semantic attempts and edge outcomes, not
120
+ real timestamps; simulation must never invoke the handler, connector, or
121
+ network. Inspect the named template and safe run trail in Sandbox preview,
122
+ then run scoped gates, exact-source `auto-review`, and host eject. The agent
123
+ cannot approve the spec, install code into the running server, or push a
124
+ repository from the Sandbox.
125
+
42
126
  ## Graph contract
43
127
 
44
128
  Version one is a bounded DAG. The graph contains:
@@ -47,9 +131,11 @@ Version one is a bounded DAG. The graph contains:
47
131
  - `agent`: calls one exact immutable agent revision and validates structured
48
132
  output.
49
133
  - `agent-decision`: produces one schema-valid `pass` or `fail` outcome.
134
+ - `typed-decision`: routes one bounded typed answer through `pass` or `fail`.
50
135
  - `gate`: evaluates the versioned allowlisted logic language.
51
136
  - `validator`: validates an envelope against a pinned JSON schema.
52
137
  - `action`: calls one exact registered action contract version.
138
+ - `human-approval`: waits for an `approvals.core` request to resolve.
53
139
  - `merge`: waits for all declared incoming paths.
54
140
  - `output`: settles the workflow with a typed result.
55
141
 
@@ -19,7 +19,7 @@ also ship a ready business agent through `defineAgent()`, read
19
19
  ## 1. The contract in code
20
20
 
21
21
  - Composition: `PlatformServerContext` (`modules/auth/src/server/composition.ts`) carries `agentTools: PlatformToolRegistry` (`register(tools)`, `list()`; `packages/kernel/src/tool-registry.ts`; a duplicate tool id throws at boot), `settings: ModuleSettingsRuntime`, and `capabilities: PlatformCapabilityRegistry` (`register(id, service)`, `get(id)`, `has(id)`; `packages/kernel/src/capability-registry.ts`). `platform/octane.config.ts` creates the registries, passes them to every module's `createServerComposition`, declares each `settings`, owns each `dispose`, then calls each `start`.
22
- - Ordering is a non-issue: `agents.core` (`modules/agents/src/platform.ts`) passes `tools: () => context.agentTools.list()` into `createAgentRuntime`, and the harness is built lazily in `start()`, which runs after every module has composed. Tools any module registers during its own compose are therefore visible, whatever the module order.
22
+ - Ordering is a non-issue: `agents.core` (`modules/agents/src/platform.ts`) passes `tools: () => context.agentTools.list()` into `createAgentRuntime`, and the harness is built when `agents.core` opens, on its first request, capability call or `startWorker()`, all of which run after every module has composed. Tools any module registers during its own compose are therefore visible, whatever the module order.
23
23
  - Helpers: import `defineApiAgentTool` from `@flowdular/sdk/harness/tool-adapters` and the types `AgentTool`, `AgentToolContext` from `@flowdular/sdk/harness/runtime`. Both subpaths are free of the Vercel AI SDK; only the harness root (`@flowdular/sdk/harness`) and `@flowdular/sdk/modules/agents/server` pull it. `defineApiAgentTool` returns a frozen `AgentTool { id, transport: 'api', target, description, requiredPermissions, inputSchema?, execute }`. `defineCliAgentTool({ id, capability: { id, risk }, ... })` wraps a CLI capability and throws at definition time for `external` or `destructive` risk.
24
24
  - Skills inside `agents.core` are tenant database records behind `agents.skills.*`, appended to agent instructions. They are unrelated to `.ai/skills/**`, which are files for coding agents.
25
25
  - A read tool's output can also become a resolvable `{{ variable }}` for variable-aware fields: the tool's `requiredPermissions` is the variable's scope mask. Register a source on `platformVariableRegistry(context.capabilities)`, require an explicit record binding, and invoke the tool with the trusted tenant, actor permission snapshot, and signal. See the `variables` skill for the complete refusal contract.
@@ -57,7 +57,7 @@ Recipe in `modules/auth/tests/endpoints.test.ts`: build the runtime with a `Data
57
57
 
58
58
  - 401 without a cookie or token.
59
59
  - 403 with a principal that lacks the permission.
60
- - Cross-tenant read returns an empty list (service level, on the suite's test provider under the non-bypass `coreloom_runtime` role).
60
+ - Cross-tenant read returns an empty list (service level, on the suite's test provider under the non-bypass `flowdular_runtime` role).
61
61
  - Mutation without `x-csrf-token` returns 403 `CSRF_REJECTED`; without `origin` returns 403.
62
62
  - Each validation bound returns 400 with its code.
63
63
 
@@ -88,12 +88,12 @@ PostgreSQL tenant-owned tables also use database-enforced isolation:
88
88
 
89
89
  - enable and force row-level security on the table;
90
90
  - define a policy whose `USING` and `WITH CHECK` clauses compare `tenant_id`
91
- with `current_setting('coreloom.tenant_id', true)`;
91
+ with `current_setting('flowdular.tenant_id', true)`;
92
92
  - run application traffic under a role that is neither a superuser nor granted
93
93
  `BYPASSRLS`;
94
94
  - use a separate migration role for DDL or policy ownership when required.
95
95
 
96
- The adapter sets `coreloom.tenant_id` with parameterized `set_config(..., true)`
96
+ The adapter sets `flowdular.tenant_id` with parameterized `set_config(..., true)`
97
97
  after `BEGIN` on the pinned connection. Never use an unpinned root query.
98
98
  Explicit tenant predicates remain required as defense in depth.
99
99
 
@@ -133,7 +133,7 @@ transactions without `tenantId`, fail with `TENANT_CONTEXT_REQUIRED`.
133
133
 
134
134
  Use `DatabaseMigration` and `runDatabaseMigrations` from `@flowdular/sdk/database`.
135
135
  Each migration has one immutable id and its PostgreSQL SQL. The ledger is
136
- `_coreloom_migrations_v2`, keyed by module namespace and migration id; its
136
+ `_flowdular_migrations_v2`, keyed by module namespace and migration id; its
137
137
  checksum covers that exact SQL.
138
138
 
139
139
  `inspectExisting(database)` is the only pre-ledger adoption proof. Use
@@ -157,8 +157,8 @@ constant. A migration-only task uses `migration-authoring` in a separate phase.
157
157
  ## 7. Tests run on a real PostgreSQL
158
158
 
159
159
  `createTestDatabaseProvider()` from `@flowdular/sdk/database-testing` gives a suite its
160
- own PostgreSQL in process by default, with the same `coreloom_runtime` and
161
- `coreloom_background` roles and the same forced row-level security a deployment
160
+ own PostgreSQL in process by default, with the same `flowdular_runtime` and
161
+ `flowdular_background` roles and the same forced row-level security a deployment
162
162
  enforces. There is no server to start and no second dialect to keep green, so
163
163
  the isolation assertions run on every turn rather than behind an environment
164
164
  flag. CI selects server PostgreSQL with `FD_TEST_DATABASE_ADAPTER=postgresql`
@@ -83,8 +83,8 @@ adapter change, sandbox eject, and deployment validation.
83
83
 
84
84
  Every turn runs against a real PostgreSQL, because the embedded one starts in
85
85
  process. The sandbox and the test suites use `createTestDatabaseProvider()` from
86
- `@flowdular/sdk/database-testing`, which brings the `coreloom_runtime` and
87
- `coreloom_background` roles and forced row-level security with it.
86
+ `@flowdular/sdk/database-testing`, which brings the `flowdular_runtime` and
87
+ `flowdular_background` roles and forced row-level security with it.
88
88
 
89
89
  A target run covers tenant A and B fixtures, operations without tenant context,
90
90
  forged cross-tenant inserts, direct row-security bypass probes, concurrent
@@ -48,7 +48,7 @@ Set `FD_TRUST_PROXY` behind a load balancer. `FD_DATABASE_BACKGROUND_URL` gives
48
48
 
49
49
  ## 3. Migrations at rollout
50
50
 
51
- Migrations are module-owned, numbered, immutable once applied, and verified by checksum against the `_coreloom_migrations_v2` ledger. The commands (`packages/cli/src/runner.ts`):
51
+ Migrations are module-owned, numbered, immutable once applied, and verified by checksum against the `_flowdular_migrations_v2` ledger. The commands (`packages/cli/src/runner.ts`):
52
52
 
53
53
  ```bash
54
54
  pnpm flowdular migration status [--module <id>] # what the ledger holds
@@ -26,7 +26,7 @@ Migration SQL is checked in and immutable after release:
26
26
  ## 2. What the v2 runner guarantees
27
27
 
28
28
  `runDatabaseMigrations(database, namespace, databaseMigrations)` uses the
29
- namespaced `_coreloom_migrations_v2` ledger. A row records namespace, migration
29
+ namespaced `_flowdular_migrations_v2` ledger. A row records namespace, migration
30
30
  id, dialect id, checksum, and applied time. The checksum covers the selected
31
31
  dialect's exact SQL.
32
32
 
@@ -76,14 +76,14 @@ Every PostgreSQL tenant table includes:
76
76
  ALTER TABLE inventory_locations ENABLE ROW LEVEL SECURITY;
77
77
  ALTER TABLE inventory_locations FORCE ROW LEVEL SECURITY;
78
78
  CREATE POLICY inventory_locations_tenant_policy ON inventory_locations
79
- USING (tenant_id = current_setting('coreloom.tenant_id', true))
80
- WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
79
+ USING (tenant_id = current_setting('flowdular.tenant_id', true))
80
+ WITH CHECK (tenant_id = current_setting('flowdular.tenant_id', true));
81
81
  ```
82
82
 
83
83
  The runtime role is not a superuser and has no `BYPASSRLS`. DDL and policy
84
84
  ownership use `purpose: 'migration'`. Runtime repository calls use
85
85
  `database.transaction(operation, { tenantId, access })`; the adapter sets
86
- transaction-local `coreloom.tenant_id` on the pinned connection. Queries still
86
+ transaction-local `flowdular.tenant_id` on the pinned connection. Queries still
87
87
  include `WHERE tenant_id = ...` as defense in depth.
88
88
 
89
89
  ## 6. Add one migration
@@ -142,7 +142,7 @@ export function createServerComposition(
142
142
 
143
143
  `PlatformServerContext` also carries `databases: DatabaseProvider`, `settings: ModuleSettingsRuntime`, `agentTools: PlatformToolRegistry`, `agentDefinitions: PlatformAgentRegistry`, and `capabilities: PlatformCapabilityRegistry`. The runtime shares one lazy database initialization, uses separate migration and runtime leases, and releases the runtime lease from `dispose()`; `prepare()` remains read-only. A composition may return module settings, lifecycle hooks, agent registrations, and typed cross-module capabilities as described in the focused skills.
144
144
 
145
- Schema: write PostgreSQL SQL in `migrations/0001_inventory_core.up.sql` and mirror it byte for byte in `databaseMigrations` as `sql: { postgresql: ... }` with an `inspectExisting` built from `postgresTenantTableState(...)`. Tenant tables enable and force row-level security with an `<table>_tenant_policy` whose `USING` and `WITH CHECK` compare `tenant_id` with `current_setting('coreloom.tenant_id', true)`; the runtime role has no superuser or `BYPASSRLS`. Repository operations use `database.transaction(..., { tenantId, access })` and retain explicit tenant predicates. Details in `migration-authoring` and `database-adapter`.
145
+ Schema: write PostgreSQL SQL in `migrations/0001_inventory_core.up.sql` and mirror it byte for byte in `databaseMigrations` as `sql: { postgresql: ... }` with an `inspectExisting` built from `postgresTenantTableState(...)`. Tenant tables enable and force row-level security with an `<table>_tenant_policy` whose `USING` and `WITH CHECK` compare `tenant_id` with `current_setting('flowdular.tenant_id', true)`; the runtime role has no superuser or `BYPASSRLS`. Repository operations use `database.transaction(..., { tenantId, access })` and retain explicit tenant predicates. Details in `migration-authoring` and `database-adapter`.
146
146
 
147
147
  Errors: `{ error: { code, message } }`; service errors `class XServiceError extends Error { constructor(readonly code: string, message: string, readonly status = 400) }`; a `failure(error)` helper routes them to `jsonResponse(..., error.status)` and everything else to `problemResponse(error, 'The <module> operation failed.')`.
148
148
 
@@ -45,8 +45,12 @@ is a new spec-authoring step and needs approval after that edit.
45
45
  ## 3. Sandbox path
46
46
 
47
47
  In the sandbox, use the operator approval action for the selected session module.
48
- The live route is POST /sandbox/api/sessions/:id/approve, exposed by
49
- approveSpecification in packages/sandbox/src/client/api.ts.
48
+ The live route is POST /sandbox/api/sessions/:id/approve with
49
+ { "module", "specHash" }, exposed by approveSpecification in
50
+ packages/sandbox/src/client/api.ts. specHash is the SHA-256 the review card
51
+ shows for the text it renders. The route refuses with 409 SPEC_CHANGED when the
52
+ current text has another hash, and with 409 QUESTIONS_PENDING while the module
53
+ has unanswered questions; it records nothing then.
50
54
 
51
55
  The route changes the status presentation and records the SHA-256 hash of the
52
56
  exact approved text in the session. Do not patch the session workspace file to
@@ -68,7 +68,7 @@ In the sandbox, end the reply with exactly one fenced block tagged `questions`,
68
68
  ```
69
69
  ````
70
70
 
71
- The sandbox renders it as a form and the answers return in the next turn as a `Decisions` section. Outside the sandbox: in Claude Code ask through the question tool with the same options, and in Codex ask in plain text with the options numbered. In every host, `recommended` is the default from the card, and an unanswered question stays a question, never a guess.
71
+ The sandbox enforces the bounds: at most 12 questions, each 1 to 400 characters; at most 8 options per question, each 1 to 120 characters; `recommended` is one of the options; no line breaks inside a value; the whole block at most 8000 characters. A block outside them comes back to you once with the reason. The sandbox renders it as a form and the answers return in the next turn as a `Decisions` section. Outside the sandbox: in Claude Code ask through the question tool with the same options, and in Codex ask in plain text with the options numbered. In every host, `recommended` is the default from the card, and an unanswered question stays a question, never a guess.
72
72
 
73
73
  When the answers come back, copy each one into `decisions[]` with `decidedBy: user` and the answer text, and update whatever the answer changed.
74
74
 
@@ -86,7 +86,7 @@ A number the case needs (a score, a premium, a price per square metre) is an `ac
86
86
 
87
87
  `modules/<dir>/spec/module.yaml`, `schemaVersion: 2`, `status: draft`. Keep the v1 keys (`id`, `specVersion`, `name`, `description`, `profile`, `capabilities`, `dependencies`, `tenancy`, `locales`, `invariants`, `permissions`, `dataOwnership`, `acceptanceScenarios`) and add the v2 arrays:
88
88
 
89
- - `entities[]`: `{ id, name, fields[], states? }`. A field is `{ id, type, required?, unique?, maxLength?, values?, reference?, description? }`. `type` is one of `string`, `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `enum`, `reference`, `json`; `unique` is `tenant` or `none`. `enum` needs `values`, `reference` needs `reference`. Entity and screen ids are `^[a-z][a-z0-9-]*$`; field and setting keys are `^[a-z][a-zA-Z0-9]*$`. Money is `integer` minor units plus an explicit currency field, never `decimal`.
89
+ - `entities[]`: `{ id, name, fields[], states? }`. A field is `{ id, type, required?, unique?, maxLength?, values?, reference?, description? }`. `type` is one of `string`, `text`, `integer`, `decimal`, `boolean`, `date`, `datetime`, `enum`, `reference`, `json`; `unique` is `tenant` or `none`. `enum` needs `values`, `reference` needs `reference`. Entity and screen ids are `^[a-z][a-z0-9-]*$`; field and setting keys are `^[a-z][a-zA-Z0-9]*$`. A field never repeats a column every tenant table owns (`id`, `tenantId`, `createdAt`; a screen may still list `createdAt`), and its key in snake case is never a PostgreSQL reserved word (`order`, `user`, `currentUser`): `spec-schema` refuses either with `SPEC_FIELD_RESERVED`. Money is `integer` minor units plus an explicit currency field, never `decimal`.
90
90
  - `screens[]`: `{ id, kind: list|record|form|dashboard, entity?, title?, columns?, filters?, navigationGroup? }`. `navigationGroup` is one of the six values on the card.
91
91
  - `actions[]`: `{ id, entity?, permission, kind: create|update|delete|custom, risk, idempotent, description }`. `risk: external` is refused by the platform, so an action may not declare it.
92
92
  - `widgets[]`: `{ id, slot, entity?, description }`; `slot` is one of the four workspace slots.
@@ -15,7 +15,7 @@ description: >-
15
15
 
16
16
  ## 2. Repositories on an embedded PostgreSQL
17
17
 
18
- `createPgliteTestProvider()` from `@flowdular/sdk/database-testing` runs a real PostgreSQL inside the test process, with the same `coreloom_runtime` and `coreloom_background` roles and the same forced row-level security a deployment enforces. Booting it costs about two seconds, so a suite opens one provider per test file, migrates it once, and truncates the module's tables between cases; `.ai/references/catalog/tests/support/database.ts` is the shape (`createCatalogTestDatabase` hands out a lease per fixture, `closeCatalogTestDatabases` runs in `afterAll`). Build the service on top: `new CatalogService((await createCatalogTestDatabase()).repository)`. A database module also keeps `tests/migrations.test.ts` for SQL byte parity, fresh apply, safe pre-ledger adoption, and a clean second start. A module whose spec has no `database` capability gets a `MemoryXRepository` from the scaffold instead; a module with a database tests the database repository, never a hand-written fake, because the SQL, the ledger and the row-level security are what need testing.
18
+ `createPgliteTestProvider()` from `@flowdular/sdk/database-testing` runs a real PostgreSQL inside the test process, with the same `flowdular_runtime` and `flowdular_background` roles and the same forced row-level security a deployment enforces. Booting it costs about two seconds, so a suite opens one provider per test file, migrates it once, and truncates the module's tables between cases; `.ai/references/catalog/tests/support/database.ts` is the shape (`createCatalogTestDatabase` hands out a lease per fixture, `closeCatalogTestDatabases` runs in `afterAll`). Build the service on top: `new CatalogService((await createCatalogTestDatabase()).repository)`. A database module also keeps `tests/migrations.test.ts` for SQL byte parity, fresh apply, safe pre-ledger adoption, and a clean second start. A module whose spec has no `database` capability gets a `MemoryXRepository` from the scaffold instead; a module with a database tests the database repository, never a hand-written fake, because the SQL, the ledger and the row-level security are what need testing.
19
19
 
20
20
  ## 3. Route recipe (from `modules/auth/tests/endpoints.test.ts`)
21
21
 
@@ -55,7 +55,7 @@ The auth middleware must have set the principal for `endpointIdentityFromContext
55
55
  - 403 on a mutation without `x-csrf-token` (`CSRF_REJECTED`) and without `origin` (`ORIGIN_REQUIRED`).
56
56
  - 400 with the stable code for each validation bound (`INVALID_INPUT`, module codes such as `INVALID_ITEM_KIND`).
57
57
  - 409 for the tenant-scoped uniqueness rule, and success for the same key in another tenant.
58
- - Tenant isolation: rows created for `tenant-a` are invisible to `list('tenant-b')`. The provider hands the suite the non-bypass `coreloom_runtime` role, so this runs against real forced row-level security; also assert that a call without tenant context fails with `TENANT_CONTEXT_REQUIRED`.
58
+ - Tenant isolation: rows created for `tenant-a` are invisible to `list('tenant-b')`. The provider hands the suite the non-bypass `flowdular_runtime` role, so this runs against real forced row-level security; also assert that a call without tenant context fails with `TENANT_CONTEXT_REQUIRED`.
59
59
  - Identity: `moduleDefinition.manifest.id` equals the module id (keeps `module.json` and `src/index.ts` aligned). The scaffold writes this and the isolation case; everything else in this list is yours.
60
60
 
61
61
  Assert at the observation boundary: status code, `error.code`, returned record fields. Do not assert internal helper names, call order, or SQL text.