create-flowdular 0.2.6 → 0.3.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 (103) hide show
  1. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  2. package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/cli-extension/SKILL.md +1 -1
  4. package/agent-template/.agents/skills/deploy-operate/SKILL.md +114 -0
  5. package/agent-template/.agents/skills/module-new/SKILL.md +29 -0
  6. package/agent-template/.agents/skills/module-update/SKILL.md +9 -1
  7. package/agent-template/.agents/skills/spec-interview/SKILL.md +114 -0
  8. package/agent-template/.agents/skills/ux-design/SKILL.md +34 -3
  9. package/agent-template/.ai/README.md +2 -1
  10. package/agent-template/.ai/agents/sandbox/business-manager.md +5 -1
  11. package/agent-template/.ai/blueprints/author-spec/README.md +1 -1
  12. package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
  13. package/agent-template/.ai/blueprints/author-spec/steps.yaml +5 -5
  14. package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +99 -3
  15. package/agent-template/.ai/blueprints/edit-module/gates.yaml +4 -0
  16. package/agent-template/.ai/blueprints/edit-module/required-files.yaml +9 -0
  17. package/agent-template/.ai/blueprints/new-module/gates.yaml +4 -0
  18. package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
  19. package/agent-template/.ai/blueprints/release/gates.yaml +4 -0
  20. package/agent-template/.ai/platform-capabilities.md +128 -0
  21. package/agent-template/.ai/policies/capabilities.yaml +130 -3
  22. package/agent-template/.ai/policies/path-ownership.yaml +5 -2
  23. package/agent-template/.ai/policies/task-budgets.yaml +5 -3
  24. package/agent-template/.ai/references/catalog/module.json +4 -4
  25. package/agent-template/.ai/references/catalog/package.json +2 -2
  26. package/agent-template/.ai/references/catalog/spec/module.yaml +5 -3
  27. package/agent-template/.ai/references/catalog/src/platform.ts +2 -0
  28. package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +89 -1
  29. package/agent-template/.ai/references/catalog/src/services/data-classes.ts +47 -0
  30. package/agent-template/.ai/references/catalog/src/services/database-repository.ts +98 -1
  31. package/agent-template/.ai/references/catalog/src/services/repository.ts +22 -1
  32. package/agent-template/.ai/references/catalog/tests/data-classes.test.ts +157 -0
  33. package/agent-template/.ai/references/catalog.provenance.json +12 -10
  34. package/agent-template/.ai/rules/flowdular.md +4 -0
  35. package/agent-template/.ai/skills/README.md +10 -0
  36. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -2
  37. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  38. package/agent-template/.ai/skills/business-agent-design/SKILL.md +0 -1
  39. package/agent-template/.ai/skills/cli-extension/SKILL.md +1 -1
  40. package/agent-template/.ai/skills/deploy-operate/SKILL.md +119 -0
  41. package/agent-template/.ai/skills/module-new/SKILL.md +29 -3
  42. package/agent-template/.ai/skills/module-update/SKILL.md +9 -3
  43. package/agent-template/.ai/skills/perf-audit/SKILL.md +0 -1
  44. package/agent-template/.ai/skills/release-eject-pr/SKILL.md +0 -1
  45. package/agent-template/.ai/skills/spec-interview/SKILL.md +120 -0
  46. package/agent-template/.ai/skills/test-hardening/SKILL.md +1 -0
  47. package/agent-template/.ai/skills/ux-design/SKILL.md +34 -3
  48. package/agent-template/.ai/skills/variables/SKILL.md +0 -2
  49. package/agent-template/.ai/skills/workflow-development/SKILL.md +0 -1
  50. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  51. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  52. package/agent-template/.claude/skills/cli-extension/SKILL.md +1 -1
  53. package/agent-template/.claude/skills/deploy-operate/SKILL.md +114 -0
  54. package/agent-template/.claude/skills/module-new/SKILL.md +29 -0
  55. package/agent-template/.claude/skills/module-update/SKILL.md +9 -1
  56. package/agent-template/.claude/skills/spec-interview/SKILL.md +114 -0
  57. package/agent-template/.claude/skills/ux-design/SKILL.md +34 -3
  58. package/agent-template/AGENTS.md +4 -0
  59. package/agent-template/CLAUDE.md +4 -0
  60. package/agent-template/docs/adr/0003-module-settings.md +1 -1
  61. package/agent-template/docs/adr/0006-agentic-workflows.md +24 -21
  62. package/agent-template/docs/agent-contract.md +2 -2
  63. package/agent-template/docs/cli-extensions.md +82 -0
  64. package/agent-template/docs/cli.md +195 -0
  65. package/agent-template/docs/configuration.md +593 -36
  66. package/agent-template/docs/design-system.md +185 -31
  67. package/agent-template/docs/getting-started.md +118 -0
  68. package/agent-template/docs/module-distribution.md +96 -0
  69. package/agent-template/docs/module-web-surfaces.md +221 -0
  70. package/agent-template/docs/modules.md +216 -0
  71. package/agent-template/docs/operations.md +545 -0
  72. package/agent-template/docs/sandbox.md +212 -0
  73. package/agent-template/platform/scripts/build.mjs +11 -0
  74. package/dist/bin.js +29 -0
  75. package/package.json +1 -1
  76. package/template/default/.dockerignore +14 -0
  77. package/template/default/.env.example +96 -0
  78. package/template/default/README.md +37 -1
  79. package/template/default/flowdular.json +15 -4
  80. package/template/default/infra/README.md +116 -0
  81. package/template/default/infra/docker/Dockerfile +37 -0
  82. package/template/default/infra/docker/compose.yaml +158 -0
  83. package/template/default/infra/docker/postgres/10-roles.sh +31 -0
  84. package/template/default/infra/docker/postgres/tls-init.sh +28 -0
  85. package/template/default/infra/kubernetes/database-secret.example.yaml +15 -0
  86. package/template/default/infra/kubernetes/deployment.yaml +211 -0
  87. package/template/default/infra/kubernetes/kustomization.yaml +9 -0
  88. package/template/default/infra/kubernetes/secrets.example.yaml +52 -0
  89. package/template/default/infra/kubernetes/service.yaml +13 -0
  90. package/template/default/modules/example/module.json +2 -1
  91. package/template/default/modules/example/package.json +1 -1
  92. package/template/default/modules/example/spec/module.yaml +1 -1
  93. package/template/default/modules/example/src/services/database-repository.ts +2 -12
  94. package/template/default/package.json +3 -2
  95. package/template/default/platform/octane.config.ts +99 -9
  96. package/template/default/platform/package.json +1 -1
  97. package/template/default/platform/src/generated/modules.client.ts +26 -2
  98. package/template/default/platform/src/generated/modules.server.ts +241 -10
  99. package/template/default/platform/src/server/health.ts +47 -0
  100. package/template/default/platform/src/server/metrics.ts +100 -0
  101. package/template/default/platform/src/server/storage.ts +172 -0
  102. package/template/default/platform/src/server/tracing.ts +85 -0
  103. package/template/default/specs/application.yaml +15 -0
@@ -119,7 +119,7 @@ Dependencies: `package.json` gets `"@flowdular/sdk/harness": "workspace:*"`. You
119
119
  - A mutating tool with `idempotency: 'required'` is executable only after the target module implements a durable ledger and the definition declares `idempotencyProtection: 'target-ledger'`. The harness derives a stable key from the durable run id and deterministic tool-call ordinal. The target ledger binds `(tenant, tool id, key)` to a canonical input hash and the first result. A replay returns that result without another mutation; the same key with another tool or input fails closed. Provider tool-call ids are audit metadata only. Never add the declaration before the target migration, repository transaction, and crash-recovery test exist.
120
120
  - Each call has a deadline (`tool.timeoutMs`, default 30 s, range 250 to 600000) and the harness caps serialized output at 32 KB (`boundToolOutput`), marking `truncated`. Still page or limit your rows so one call cannot dominate the run window.
121
121
  - Events per call land in the run's persisted audit chain: `tool.started`, then `tool.completed` (metadata `tool`, `outputCharacters`, `truncated`) on success, `tool.failed` (reason) on error, or `tool.denied` (reason) when not granted or input-invalid.
122
- - Runs are enqueued by `POST /api/agent-runs` behind `agents.runs.execute`, claimed by `AgentWorker` with a lease, observed through `GET /api/agent-runs` and the SSE stream. The registered tool ids surface in `GET /api/agents` `tools`, which the Agents form reads to build the allowed-tools grid. Never make a tool block on user input.
122
+ - Runs are enqueued by `POST /api/agent-runs` behind `agents.runs.execute`, claimed by `AgentWorker` with a lease, observed through `GET /api/agent-runs` and the SSE stream. The registered tool ids surface in `GET /api/agents/context` `tools`, which the Agents form reads to build the allowed-tools grid. Never make a tool block on user input.
123
123
 
124
124
  ## 4. Deliverables for a module
125
125
 
@@ -20,7 +20,7 @@ Check every route in `src/api/endpoints.ts` against `.ai/references/catalog/src/
20
20
 
21
21
  ## 2. Scope model
22
22
 
23
- `modules/auth/src/acl/scopes.ts`: `AUTH_SCOPES`, `PLATFORM_SCOPES` (`system.workspace.access` gates the shell in `platform/src/App.tsrx`; `system.settings.read` and `system.settings.manage` guard `GET /api/settings` and `POST /api/settings/update` in `modules/auth/src/server/settings-endpoints.ts`), `BUNDLED_MODULE_SCOPES`, `OWNER_SCOPES` (all of them), `MEMBER_SCOPES` (read scopes plus `agents.runs.execute`). Sign-up creates an owner with `OWNER_SCOPES`; member creation copies `OWNER_SCOPES` or `MEMBER_SCOPES` by role (`modules/auth/src/services/auth-service.ts`). A module's scopes reach existing owners through `pnpm flowdular module enable <id> --apply` (which runs the grant) or `pnpm flowdular auth sync-scopes --module <id> --apply` for a re-grant. Navigation in the `Development` group is owner-only in the client (`packages/client/src/shell/navigation.ts`); the server permission stays authoritative.
23
+ `modules/auth/src/acl/scopes.ts`: `AUTH_SCOPES`, `PLATFORM_SCOPES` (`system.workspace.access` gates the shell in `platform/src/App.tsrx`; `system.settings.read` and `system.settings.manage` guard `GET /api/settings` (`system.settings.list`) and `POST /api/settings/update` (`system.settings.update`) in `modules/system/src/server/endpoints.ts`, from `SYSTEM_PERMISSIONS` in `modules/system/src/acl/permissions.ts`), `BUNDLED_MODULE_SCOPES`, `OWNER_SCOPES` (all of them), `MEMBER_SCOPES` (read scopes plus `agents.runs.execute`). Sign-up creates an owner with `OWNER_SCOPES`; member creation copies `OWNER_SCOPES` or `MEMBER_SCOPES` by role (`modules/auth/src/services/auth-service.ts`). A module's scopes reach existing owners through `pnpm flowdular module enable <id> --apply` (which runs the grant) or `pnpm flowdular auth sync-scopes --module <id> --apply` for a re-grant. Navigation in the `Development` group is owner-only in the client (`packages/client/src/shell/navigation.ts`); the server permission stays authoritative.
24
24
 
25
25
  Review question: does every new scope appear in the spec `permissions`, in `src/acl/permissions.ts`, on the endpoint, and on the client contribution that exposes it?
26
26
 
@@ -85,7 +85,7 @@ export default cliExtension;
85
85
 
86
86
  ## 4. What the runner does with the descriptor (`packages/cli/src/runner.ts`, `runExtensionCommand`)
87
87
 
88
- - `risk: 'external'`: refused with `APPROVAL_VERIFIER_REQUIRED`. `risk: 'destructive'` without `localOnly`: the same.
88
+ - `risk: 'external'`, or `'destructive'` without `localOnly`: refused with `APPROVAL_VERIFIER_REQUIRED` unless `--grant <token> --tenant <id>` carries a verified approval grant for this capability and invocation digest (`APPROVAL_GRANT_INVALID`, `APPROVAL_GRANT_EXPIRED`, `APPROVAL_GRANT_MISMATCH` otherwise).
89
89
  - `localOnly: true`: refused with `LOCAL_ONLY_CAPABILITY` unless `FD_ENV` or `NODE_ENV` is `development` or `test` (unset counts as development).
90
90
  - `requiresApprovedSpec: true`: needs `--spec <path>` to a schema-valid spec with `status: approved`, otherwise `APPROVED_SPEC_REQUIRED`, `SPEC_VALIDATION_FAILED` or `SPEC_NOT_APPROVED`.
91
91
  - `destructive` with `--apply`: needs `--confirm <confirmation>` (`CONFIRMATION_REQUIRED`).
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: deploy-operate
3
+ description: >-
4
+ Build, configure, roll out and operate a Flowdular application: the container
5
+ image, the production environment keys, migrations at rollout, health and
6
+ readiness, backup and restore, and rollback.
7
+ ---
8
+ # Deploy and operate
9
+
10
+ This is host work, not sandbox work. A sandbox specialist has no network, no git and no deployment.
11
+
12
+ ## 1. Container build
13
+
14
+ `infra/docker/Dockerfile` is a two-stage build on `node:24-bookworm-slim`. The builder enables corepack, pins `pnpm@11.17.0`, runs `pnpm install --frozen-lockfile`, then `pnpm verify && pnpm build`, so a failing gate fails the image. The runtime stage copies only `platform/dist` and `platform/package.json`, runs as the non-root user `octane` (uid 1001), exposes 3000, declares the `/data` volume and starts `node platform/dist/server/entry.js`. Its `HEALTHCHECK` polls `/api/health`.
15
+
16
+ ```bash
17
+ docker compose -f infra/docker/compose.yaml up --build # local stack: postgres-tls, postgres 17 with ssl=on, app
18
+ ```
19
+
20
+ The compose `app` service runs `read_only: true` with a `tmpfs` on `/tmp` and publishes `${FD_PORT:-3000}`. Tagging `v*.*.*` (or running the workflow by hand) builds and pushes the same Dockerfile to `ghcr.io/<repository>` through `.github/workflows/container.yml`. Kubernetes manifests live in `infra/kubernetes` and apply with `kubectl apply -k infra/kubernetes`; the secrets they expect have examples in the same directory. `infra/README.md` carries the local and cluster walkthrough.
21
+
22
+ ## 2. Production environment
23
+
24
+ `docs/configuration.md` is the reference. The keys a production deployment must set:
25
+
26
+ | Key | Why |
27
+ | ------------------------------- | --------------------------------------------------------------------------------------------------- |
28
+ | `NODE_ENV=production` | Turns on every production refusal below; `FD_ENV=production` only gates the CLI local-only commands |
29
+ | `FD_DATABASE_ADAPTER` | Must not be `pglite`; production refuses the embedded adapter |
30
+ | `FD_DATABASE_URL` | Runtime role, neither `SUPERUSER` nor `BYPASSRLS` |
31
+ | `FD_DATABASE_MIGRATOR_URL` | Separate DDL role; required in production |
32
+ | `FD_DATABASE_TLS=verify-full` | The only value production accepts, with `FD_DATABASE_TLS_CA[_FILE]` |
33
+ | `FD_AUTH_PUBLIC_ORIGIN` | Same-origin checks and cookie scope |
34
+ | `FD_AUTH_SECURE_COOKIE` | `Secure` and the `__Host-` cookie prefix; defaults on in production |
35
+ | `FD_AUTH_MFA_KEY` | Decrypts stored MFA secrets |
36
+ | `FD_AGENT_CREDENTIAL_KEY` | `agents.core` refuses to boot without it |
37
+ | `FD_AGENT_RUN_GRANT_KEY` | `agents.core` refuses to boot without it |
38
+ | `FD_WORKFLOWS_PAYLOAD_KEY` | `workflows.core` refuses to boot without it |
39
+ | `FD_WORKFLOWS_CURSOR_KEY` | `workflows.core` refuses to boot without it |
40
+ | `FD_AUTOMATIONS_CREDENTIAL_KEY` | `automations.core` refuses to boot without it |
41
+ | `FD_NOTIFICATIONS_SECRET_KEY` | `notifications.core` refuses to boot without it |
42
+ | `FD_CONNECTORS_SECRET_KEY` | `connectors.core` refuses to boot without it |
43
+ | `FD_AUDIT_ANCHOR_KEY` | `audit.core` refuses to seal or verify without it in production |
44
+
45
+ Every key above is base64 of 32 random bytes where it names a key. `FD_AUTOMATIONS_CREDENTIAL_KEY` is read by `modules/automations/src/services/secret-vault.ts`, which throws `FD_AUTOMATIONS_CREDENTIAL_KEY is required in production.`; in development it generates and persists a local key file instead, so a deployment that never set it fails only at the production boot. Set it whenever `automations.core` is enabled in `flowdular.json`.
46
+
47
+ Set `FD_TRUST_PROXY` behind a load balancer. `FD_DATABASE_BACKGROUND_URL` gives worker traffic its own pool. Everything else in `docs/configuration.md` has a working default.
48
+
49
+ ## 3. Migrations at rollout
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`):
52
+
53
+ ```bash
54
+ pnpm flowdular migration status [--module <id>] # what the ledger holds
55
+ pnpm flowdular migration verify # checksums against the shipped SQL
56
+ pnpm flowdular migration apply --module <id> # dry run, one module at a time
57
+ pnpm flowdular migration apply --module <id> --apply # writes
58
+ ```
59
+
60
+ `migration apply` carries the `migration.apply.local` capability, which is `localOnly`: the runner refuses it with `LOCAL_ONLY_CAPABILITY` unless `FD_ENV` or `NODE_ENV` is `development` or `test`. It is therefore a local and staging tool as the code stands, not a production rollout step. In production the application applies its own module migrations at boot under a migration lease using `FD_DATABASE_MIGRATOR_URL`, so the rollout order is: apply the new image, let it migrate, then verify. A new module needs `pnpm flowdular module enable <id> --apply` (which grants its scopes) in the workspace before the image is built.
61
+
62
+ Never edit an applied `.up.sql`, not even whitespace: the checksum changes and the next boot refuses to start.
63
+
64
+ ## 4. Health and readiness
65
+
66
+ - `GET /api/health` is public and static. It proves the process is up and nothing else. Use it as the liveness probe.
67
+ - `GET /api/ready` is public and calls the database provider: it checks the runtime and background roles are neither superuser nor `BYPASSRLS`, then runs a statement on the migrator pool. It answers 503 with `retry-after: 1` when the database is unavailable. Use it as the readiness probe.
68
+
69
+ Both are served by `platform/src/server/health.ts`. The shipped Docker and Kubernetes probes all point at `/api/health`; moving the readiness probe to `/api/ready` is the improvement worth making. Before the database is configured the application runs in setup mode and `/api/ready` is not routed at all, so a first-run container answers 404 there rather than 503.
70
+
71
+ ## 5. Backup, restore, and the key trap
72
+
73
+ `docs/operations.md` is the runbook. Read it before touching production data; this skill does not restate it. The commands:
74
+
75
+ ```bash
76
+ pnpm flowdular database backup --output <dir> # dry run
77
+ pnpm flowdular database backup --output <dir> --apply
78
+ pnpm flowdular database restore --input <dir> --apply --confirm restore-database
79
+ pnpm flowdular database restore-production --input <dir> --target <db> --grant <token> --tenant <id> [--platform-url <origin>|--platform-stopped] --apply --confirm restore-database
80
+ ```
81
+
82
+ Production restores run `database.restore.production`: an approval grant bound to the exact flags, `--target` equal to the migrator DSN database, a separate migrator DSN, a key mismatch refused unless the approval included `--allow-key-mismatch`, and a refusal while the health endpoint answers. PITR for the compose stack is `infra/docker/pitr.sh` (see `infra/README.md`); in Kubernetes it is the managed provider's job.
83
+
84
+ The trap: **the encryption keys live outside the database.** Agent provider credentials, agent run grants, workflow payloads and cursors, automation secrets, MFA secrets, stored objects and connector credentials are all stored as ciphertext, and the keys are environment variables (`FD_AGENT_CREDENTIAL_KEY`, `FD_AGENT_RUN_GRANT_KEY`, `FD_WORKFLOWS_PAYLOAD_KEY`, `FD_WORKFLOWS_CURSOR_KEY`, `FD_AUTOMATIONS_CREDENTIAL_KEY`, `FD_AUTH_MFA_KEY`, `FD_NOTIFICATIONS_SECRET_KEY`, `FD_STORAGE_ENCRYPTION_KEY`, `FD_CONNECTORS_SECRET_KEY`, `FD_AUDIT_ANCHOR_KEY`). A database backup without the matching keys restores rows nobody can read, and rotating a key without re-encrypting orphans everything encrypted under the old one; every sealing key has a `secrets-rotate` command in the runbook's rotation table, the storage key two (`documents` and `exports`). Back the keys up separately, restore them together with the dump, and record which key version a dump belongs to.
85
+
86
+ ## 6. Rollback
87
+
88
+ 1. Redeploy the previous image tag. Module migrations are additive (`CREATE TABLE IF NOT EXISTS`, `ADD COLUMN`), so an older image runs against a newer schema; it ignores columns it does not know.
89
+ 2. Never run a `.down.sql` to roll back a release. They document the reverse for review, and applying one against live data is data loss.
90
+ 3. To take one module out of service without a redeploy: `pnpm flowdular module disable <id> --apply` (it is a dry run without `--apply`), then rebuild the composition and redeploy. Its tables stay.
91
+ 4. If the rollback is because of a key change, restore the previous key first; the image alone will not fix unreadable ciphertext.
92
+
93
+ - For data loss between two dumps use PITR (`pitr.sh restore --target-time`), never a `.down.sql`.
94
+
95
+ ## 7. Production checklist
96
+
97
+ - `pnpm verify` and `pnpm build` pass on the commit being shipped (the image build runs both).
98
+ - `NODE_ENV=production`, a PostgreSQL adapter, `FD_DATABASE_TLS=verify-full` with its CA.
99
+ - Runtime role has neither `SUPERUSER` nor `BYPASSRLS`; the migrator role is separate.
100
+ - Every encryption key set, stored outside the database, and backed up with a recorded version.
101
+ - `FD_AUTOMATIONS_CREDENTIAL_KEY` set whenever `automations.core` is enabled in `flowdular.json`, `FD_NOTIFICATIONS_SECRET_KEY` whenever `notifications.core` is, `FD_CONNECTORS_SECRET_KEY` whenever `connectors.core` is, and `FD_AUDIT_ANCHOR_KEY` whenever `audit.core` is.
102
+ - `FD_AUTH_PUBLIC_ORIGIN` matches the public URL; `FD_AUTH_SECURE_COOKIE` on; `FD_TRUST_PROXY` set behind a proxy.
103
+ - Sign-up closed (`FD_AUTH_ALLOW_SIGN_UP`) unless the deployment is public; the first owner created with `flowdular auth workspace-create` (see `docs/cli.md`).
104
+ - Liveness on `/api/health`, readiness on `/api/ready`.
105
+ - `pnpm flowdular migration verify` clean after rollout, and `pnpm flowdular doctor --json` reports `status: healthy`.
106
+ - A restore has been rehearsed once, keys included, and a PITR restore once from the newest base backup.
107
+
108
+ ## Pitfalls
109
+
110
+ - `setup quick` and `auth greenfield` are local resets that wipe auth data. They refuse outside `development` and `test`; never point them at a deployment.
111
+ - `pglite` is the local and test adapter. A production boot with it is refused, not degraded.
112
+ - A module added to the workspace but not enabled is absent from the built image: `module enable` writes the generated composition, and `pnpm build` bakes it in.
113
+ - Scopes for a new permission reach existing owners only after `flowdular auth sync-scopes --module <id> --apply`; an endpoint can be live while every user gets 403.
114
+ - Rotating `FD_AUTH_MFA_KEY` locks out every enrolled user, not just new enrolments.
@@ -10,6 +10,35 @@ The reference module is `.ai/references/catalog` (in a sandbox session: `referen
10
10
 
11
11
  Two ways to land the same module: the sandbox (a brief, specialist turns, gates after every turn, preview, eject) or the direct path (this skill in your own coding tool, the gates by hand, `pnpm verify`, a pull request). The sections below mark the differences.
12
12
 
13
+ ## Spec is the contract
14
+
15
+ With an approved `schemaVersion: 2` spec, the specification is the requirement document and you do not go looking for one. Read the spec, the files on this skill's touch list, and `.ai/references/catalog` for shape. Do not scan `modules/` or `packages/`; `.ai/platform-capabilities.md` answers what the platform provides, and the reference module answers what the code looks like.
16
+
17
+ Anything the spec does not say is a spec defect, not a decision you make. A missing field, an unstated conflict behaviour, an undefined state transition, a screen without columns: report it back. In the sandbox that is `HANDOFF: business-manager - <what is missing>`; on a host it is a question to the user. Never fill the gap with a plausible guess, and never implement anything listed in `outOfScope[]`.
18
+
19
+ Every `acceptanceScenarios[]` entry maps to at least one test in `tests/`. A scenario with no test is unfinished work, and the scenario id belongs in the test name so the mapping is readable.
20
+
21
+ Each spec element maps to files:
22
+
23
+ | Spec element | Files it produces |
24
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `entities[]` | `src/domain/types.ts`, `migrations/000N_<module>_<name>.{up,down}.sql`, `src/services/migration.ts`, `src/services/{repository,database-repository}.ts` |
26
+ | `entities[].fields[]` | the columns and row mapping above, the validation bounds in `src/api/endpoints.ts`, the form field and table cell in `src/client/*.tsrx` |
27
+ | `fields[].unique: tenant` | a `(tenant_id, <field>)` unique index in the migration plus the stable conflict code in the service |
28
+ | `entities[].states` | the status column, the transition guard in the service, the `Tag` tone in the view |
29
+ | `screens[]` | `src/client/<Pascal>View.tsrx`, a `views` entry and a `navigation` entry in `src/client/contribution.tsrx`, `translations/*.json` |
30
+ | `screens[].columns`/`filters` | the `TableColumn[]` outside the component, and the controls inside the `Filters` dropdown |
31
+ | `screens[].navigationGroup` | the navigation entry's `group` |
32
+ | `actions[]` | the service method with its error code, the `defineEndpoint` route, the fetch in `src/client/api.ts` |
33
+ | `widgets[]` | a widget component plus a `widgets` entry with its `slot` in `src/client/contribution.tsrx` |
34
+ | `settings[]` | `src/settings.ts` (`defineModuleSettings`) and `settings:` in `src/platform.ts` |
35
+ | `agentTools[]` | `src/agent/tools.ts` and the `context.agentTools.register` call, as a separate `agent-tool-design` phase |
36
+ | `permissions[]` | `src/acl/permissions.ts`, the endpoint `access.permission`, the client `scope` |
37
+ | `acceptanceScenarios[]` | `tests/module.test.ts` and its siblings, at least one case each |
38
+ | `outOfScope[]`, `decisions[]` | no code. Read them so you do not rebuild a decision or implement a deferred feature. |
39
+
40
+ A v1 spec stays valid and carries none of these arrays. Then the requirements are `invariants`, `permissions` and `acceptanceScenarios`, and everything the spec leaves open is still a question rather than a guess.
41
+
13
42
  ## 1. Preconditions
14
43
 
15
44
  - `pnpm flowdular doctor --json` reports `status: healthy` (repository root only; the sandbox runs gates for you).
@@ -6,6 +6,14 @@ description: >-
6
6
  ---
7
7
  # Update an existing module
8
8
 
9
+ ## Spec is the contract
10
+
11
+ With an approved `schemaVersion: 2` spec delta, the specification is the requirement document. Read the spec, the module itself, the touch list for the change class below, and `.ai/references/catalog` for shape. Do not scan `modules/` or `packages/`: `.ai/platform-capabilities.md` answers what the platform provides.
12
+
13
+ Anything the delta does not say is a spec defect, not your decision. Report it back (`HANDOFF: business-manager - <what is missing>` in the sandbox, a question to the user on a host) instead of guessing, and never implement an item the spec parks in `outOfScope[]`. Every new or changed `acceptanceScenarios[]` entry maps to at least one test, with the scenario id in the test name.
14
+
15
+ The spec-element to file mapping is the table in `module-new`; the change classes below are the same mapping arranged by what you are changing.
16
+
9
17
  ## 1. Read first
10
18
 
11
19
  Read the whole module before changing it: `spec/module.yaml`, `src/index.ts`, `src/acl/permissions.ts`, `src/api/endpoints.ts`, `src/services/*`, `src/client/*`, `tests/`. Keep every exported name in `src/index.ts`, `src/server/index.ts` and `src/client/index.ts` stable: other modules import them (`modules/users` uses `AuthRuntime` from `@flowdular/sdk/modules/auth/server`), and the generated composition imports `createServerComposition` and `createClientContribution`.
@@ -62,7 +70,7 @@ Business agent: use `business-agent-design` as a separate phase; add the approve
62
70
 
63
71
  ## 3. Versions and spec
64
72
 
65
- Bump `spec/module.yaml` `specVersion`, `module.json` `version` and `package.json` `version` together (patch for a fix, minor for a new endpoint, screen or column). Add an acceptance scenario for every new behaviour and an invariant for every new rule; the scenario id matches `^[A-Z][A-Z0-9-]+$`. In the sandbox the business manager leaves the changed spec in `draft` or `in-review`; only the operator approval route records the approved hash and permits implementation.
73
+ Bump `spec/module.yaml` `specVersion`, `module.json` `version` and `package.json` `version` together with `pnpm flowdular module version bump <id> <patch|minor|major> --apply` (patch for a fix, minor for a new endpoint, screen or column); it also retargets every dependent `^` range that stops matching. A new `context.capabilities.register` id goes under `provides` in `module.json`; a new `context.capabilities.get` id goes under `requires`. Add an acceptance scenario for every new behaviour and an invariant for every new rule; the scenario id matches `^[A-Z][A-Z0-9-]+$`. In the sandbox the business manager leaves the changed spec in `draft` or `in-review`; only the operator approval route records the approved hash and permits implementation.
66
74
 
67
75
  ## 4. Gates
68
76
 
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: spec-interview
3
+ description: >-
4
+ Turn a business request into a schema-valid v2 module specification by
5
+ proposing a platform default for every decision and asking only what cannot be
6
+ inferred, so implementation never has to guess or scan the repository.
7
+ ---
8
+ # Interview a request into a specification
9
+
10
+ The specification is the contract implementation reads instead of the repository. Everything an engineer would otherwise have to guess belongs in it: entities, fields, screens, actions, settings, tools, and what was deliberately left out. A missing decision costs a round trip later, so surface it now.
11
+
12
+ Work closed-world. `.ai/platform-capabilities.md` is the complete list of what the platform can deliver. Anything outside it is `outOfScope` with the business decision that replaces it, never a promise.
13
+
14
+ ## 1. Read exactly this
15
+
16
+ 1. `.ai/platform-capabilities.md`, the capability card (in a session: `reference/platform-capabilities.md`).
17
+ 2. `packages/contracts/schemas/module-spec.schema.json`, the shape and the enums (in a session: `reference/packages/contracts/schemas/module-spec.schema.json`).
18
+ 3. `.ai/references/catalog/spec/module.yaml`, a written example (in a session: `reference/example-module/spec/module.yaml`).
19
+
20
+ For an edit, also read the module's current `spec/module.yaml` and write the smallest delta. Do not open `modules/` or `packages/` for anything else; the card carries what you need, and a fact it lacks is a question, not a search.
21
+
22
+ ## 2. Decision checklist
23
+
24
+ One pass, in this order. For each row, write the default from the card into the spec and record it as a `decisions[]` entry with `decidedBy: default`. Ask only where the answer is a business fact that no default can supply.
25
+
26
+ | Decision | Default to propose | Lands in |
27
+ | ---------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------- |
28
+ | Actors | Owner manages, member reads | `permissions`, `invariants` |
29
+ | Entities and fields | One primary entity; `name` required, `maxLength` 120; no field the request did not name | `entities[]` |
30
+ | Uniqueness | The human-facing code is `unique: tenant`; everything else `none` | `entities[].fields[].unique` |
31
+ | States and transitions | `active` and `archived`, every transition behind the manage permission | `entities[].states` |
32
+ | Who sees what | Both permissions in the same navigation entry; the manage action hidden without the scope | `permissions`, `screens[]`, `invariants` |
33
+ | What is denied | Unauthenticated 401, missing permission 403, cross-tenant read returns nothing | `acceptanceScenarios` |
34
+ | Failure behaviour | A duplicate returns a stable conflict and changes nothing; bounds return 400 | `invariants`, `acceptanceScenarios` |
35
+ | Cross-module reads | None. A read of another module goes through its public capability and a declared dependency | `dependencies`, `dataOwnership` |
36
+ | Screens | One `list` screen with the entity's identifying columns | `screens[]` |
37
+ | Widgets | None. A count belongs on `dashboard.metrics` only when the request asks for it | `widgets[]` |
38
+ | Settings | None. A number the business may change later is `scope: tenant` with a stated default | `settings[]` |
39
+ | Agent tools | None. A tool is a later phase and `risk` may only be `read` or `workspace-write` | `agentTools[]` |
40
+ | Reports | None. There is no export, no PDF and no search; a report is a screen or it is out of scope | `outOfScope[]` |
41
+ | Out of scope | Every item from the card's gap list the request touched, each with its business decision | `outOfScope[]`, `decisions[]` |
42
+
43
+ A default you propose is still a decision: it goes into `decisions[]` so the operator can see and overturn it, and so the next agent never re-derives it.
44
+
45
+ ## 3. Ask only what you cannot infer
46
+
47
+ Ask when the answer is a business fact: who may see a price, whether a code is unique across the company or per branch, what happens to an order whose customer is deleted. Never ask what the card already answers, and never ask two questions where one choice settles both. Keep it under about six questions per turn.
48
+
49
+ In the sandbox, end the reply with exactly one fenced block tagged `questions`, nothing after it:
50
+
51
+ ````text
52
+ ```questions
53
+ {
54
+ "questions": [
55
+ {
56
+ "id": "Q-1",
57
+ "question": "Is the item code unique for the whole workspace or per warehouse?",
58
+ "options": ["Unique per workspace", "Unique per warehouse"],
59
+ "recommended": "Unique per workspace",
60
+ "allowFreeText": true
61
+ }
62
+ ]
63
+ }
64
+ ```
65
+ ````
66
+
67
+ 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.
68
+
69
+ When the answers come back, copy each one into `decisions[]` with `decidedBy: user` and the answer text, and update whatever the answer changed.
70
+
71
+ ## 4. Write the specification
72
+
73
+ `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:
74
+
75
+ - `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`.
76
+ - `screens[]`: `{ id, kind: list|record|form|dashboard, entity?, title?, columns?, filters?, navigationGroup? }`. `navigationGroup` is one of the six values on the card.
77
+ - `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.
78
+ - `widgets[]`: `{ id, slot, entity?, description }`; `slot` is one of the four workspace slots.
79
+ - `settings[]`: `{ key, type: string|integer|boolean|enum, scope: tenant|platform, default?, values?, description }`.
80
+ - `agentTools[]`: `{ id, permission, description, risk: read|workspace-write }`.
81
+ - `outOfScope[]`: plain sentences, each naming the gap and the decision taken instead.
82
+ - `decisions[]`: `{ id, question, answer, decidedBy: user|default }`; ids match `^[A-Z][A-Z0-9-]+$`, for example `D-UNIQUE-SKU`.
83
+
84
+ Put the primary entity's read and manage permissions first: the scaffold builds that entity and later permissions become constants only. Every `acceptanceScenarios[]` entry stays observable (given, when, then) and covers success, denial and the cross-tenant case, because each one becomes at least one test. The schema rejects unknown keys.
85
+
86
+ ## 5. Validate
87
+
88
+ ```bash
89
+ pnpm flowdular spec validate --all --json
90
+ ```
91
+
92
+ In the sandbox this is the `spec-schema` gate and runs for you. Fix every issue before ending the turn; a spec that does not validate cannot be approved.
93
+
94
+ ## 6. Close the turn
95
+
96
+ End with the decision list: each decision, the answer, and whether it came from the user or from a platform default. Then state plainly that implementation cannot start until the operator approves this exact specification, and that any later edit invalidates that approval. In the sandbox the operator approves the exact hash; on a host, approval is recorded only through `spec-approval` after an explicit user instruction.
97
+
98
+ Sandbox handoff: `HANDOFF: none - <the open questions>` while questions are outstanding, otherwise the next specialist with the reason.
99
+
100
+ ## Refusals
101
+
102
+ - Never write `status: approved`, and never claim a spec is approved. Approval is the operator's act.
103
+ - Never write TypeScript, `module.json`, `package.json` or any implementation file. This skill produces `spec/module.yaml` and, where the role allows, `translations/**`.
104
+ - Never invent a business fact. An unanswered question is `decisions[]` left open plus a question, not a plausible answer.
105
+ - Never promise a capability the card lists as missing. It goes to `outOfScope[]`.
106
+
107
+ ## Pitfalls
108
+
109
+ - A field nobody asked for is a cost forever. If the request did not name it, leave it out and record the omission.
110
+ - `unique: tenant` without a stated conflict behaviour produces an undefined error path; pair it with an acceptance scenario.
111
+ - `states` without `transitions` lets any state reach any other. Name the legal moves and their permission.
112
+ - A screen with no `columns` gives the engineer nothing to build; list the identifying fields in display order.
113
+ - An `enum` field with values that are really a lookup table wants its own entity instead.
114
+ - Bumping `specVersion` is part of an edit, not an afterthought; the delivery gate compares it.
@@ -60,8 +60,17 @@ Read-only master-detail (runs, playground) keeps `ui-two-col` (+ `--wide-aside`)
60
60
  - `Button`: `variant` primary, secondary (default), ghost, danger; `size` sm, md, lg; `type` button, submit; `block`; `disabled`; `onClick`.
61
61
  - `FormField`: `label`, `required`, `help`, `error`; one control child with `ui-input`, `ui-select` or `ui-textarea`.
62
62
  - `SearchField`: `value`, `placeholder`, `label` (accessible name), `onInput(value)`.
63
- - `Table`: `columns: TableColumn<Row>[]` (`key`, `header`, required `width`, `cell(row)`, `numeric`), `rows`, `rowKey(row)`, `status`, `loadingLabel`, `empty`, `emptyFiltered`, `filtered`, `actions(row): TableAction[]`, `actionsLabel`, optional stable `actionsWidth` (160 px default, 280 px for two actions), `onSelect(row)`, `selectedKey`, `caption`.
63
+ - `Table`: `columns: TableColumn<Row>[]` (`key`, `header`, required `width`, `cell(row)`, `numeric`, `value(row)` for the comparable and searchable value behind the cell), `rows`, `rowKey(row)`, `status`, `loadingLabel`, `empty`, `emptyFiltered`, `filtered`, `sorting` with `sortingState` and `onSortingChange`, `globalFilter`, `pagination` (`pageIndex`, `pageSize`, `onPageChange`, `totalRows` when the module paged in SQL), `actions(row): TableAction[]`, `actionsLabel`, optional stable `actionsWidth` (160 px default, 280 px for two actions), `onSelect(row)`, `selectedKey`, `caption`. Only a column with `value` is sortable and searched. Multi-row selection: `selection` (`selectedKeys: ReadonlySet<string>`, `onSelectionChange(keys)`, `label` of the header checkbox, `rowLabel(row)`, optional `selectable(row)` and `clearLabel`) adds a leading checkbox column whose header toggles the rows on screen; `bulkActions: TableBulkAction[]` (`id`, `label`, `icon`, `tone`, `disabled`, `reason`, `onSelect(keys)`) render in the bar above the table while something is selected, and `selectionSummary(count)` translates "N selected". The screen keeps the keys in its own state and resets them on a page, sort or filter change; the header checkbox touches only the rows on screen, while the clear button empties the whole set through `onSelectionChange`.
64
64
  - `TableCard`: every `Table` prop plus `title`, `count`, `head`, `search`, `filters`, `before`, `after`, `note`, `noteIcon`.
65
+ - `Pagination`: the pager for the `TableCard` `after` slot: `pageIndex`, `pageSize`, `totalRows` (after the screen's own filtering), `onPageChange(pageIndex)`, `pageSizes` with `onPageSizeChange(pageSize)` (both or neither), `label`, `previousLabel`, `nextLabel`, `pageSizeLabel`, `summary(range)` that the screen translates. It reads "1 of 1" over an empty set, so it is rendered unconditionally.
66
+ - `Select`: the labelled native select: required `id`, `label`, `options` (`value`, `label`, `disabled`), `value`, `onChange(value)`, `placeholder`, `name` (defaults to `id`), `required`, `disabled`, `invalid`, `help`, `error`.
67
+ - `DateField`: required `id`, `label`, ISO `value` (`YYYY-MM-DD`, or `YYYY-MM-DDTHH:mm` for `kind="datetime"`), `onChange(value)`, `kind`, `locale` from `activeLocale()`, `min`, `max`, `name`, `required`, `disabled`, `invalid`, `help`, `error`, `describedBy` for a message a group around it owns. The reading beside the input repeats the value in the reader's locale.
68
+ - `DateRangeField`: required `id`, `legend`, `fromLabel`, `toLabel`, `value` (`from`, `to`), `onChange(value)`, `kind`, `locale`, `min`, `max`, `required`, `disabled`, `reversedMessage`, `help`, `error`; each side bounds the other and a reversed range is reported, never swapped.
69
+ - `Tabs`: required `id`, `items` (`id`, `label`, `disabled`), `active`, `onChange(id)`, `label`; it renders only the tablist, and the caller renders `<id>-panel-<active>` labelled by `<id>-tab-<active>`.
70
+ - `ToastHost`: `label`, `closeLabel`, optional `store`. One per screen that raises toasts, rendered even while empty; `toasts.success`, `.error` and `.info` raise them and `createToastStore` makes a scoped queue.
71
+
72
+ `Select`, `DateField` and `DateRangeField` own their label, so they go straight into a `ui-form__row` and never inside a `FormField`.
73
+
65
74
  - `Filters`: `open`, `onToggle`, `activeCount`, `label`; children are the filter controls, which belong in the dropdown and nowhere else.
66
75
  - `CheckGrid`: `groups: { label, options: { value, label, hint? }[] }[]`, `value: string[]`, `mono`, `disabled`, `onChange(next)`.
67
76
  - `Drawer`: `open`, `title`, `subtitle`, `width` md or lg, `onClose`; child is `ui-drawer__form` or `ui-drawer__body`. Escape and the scrim close it.
@@ -74,16 +83,38 @@ Read-only master-detail (runs, playground) keeps `ui-two-col` (+ `--wide-aside`)
74
83
  - `Icon`: `name`, `size` (18 default, 16 in controls, 14 in `Button size="sm"`), `strokeWidth`.
75
84
  - `BrandMark`: `size`, `signature`, `tone`; brand moments only.
76
85
 
77
- Icon keys (`ICON_PATHS`, `packages/ui/src/icons/Icon.tsrx`): `dashboard`, `parties`, `catalog`, `user`, `users`, `shield`, `code`, `modules`, `file-text`, `play`, `bot`, `flask`, `activity`, `plug`, `search`, `chevron-down`, `chevrons-up-down`, `plus`, `panel-left`, `check`, `filter`, `download`, `more`, `external`, `alert`, `x`, `sign-out`, `refresh`, `help`, `key`, `settings`, `braces`. An unknown name renders `modules` silently, so check the list.
86
+ Icon keys (`ICON_PATHS`, `packages/ui/src/icons/Icon.tsrx`): `dashboard`, `parties`, `catalog`, `user`, `users`, `shield`, `code`, `modules`, `file-text`, `play`, `bot`, `flask`, `activity`, `plug`, `search`, `chevron-down`, `chevron-left`, `chevron-right`, `chevrons-up-down`, `sort`, `calendar`, `plus`, `panel-left`, `check`, `filter`, `download`, `more`, `external`, `alert`, `x`, `sign-out`, `refresh`, `help`, `info`, `key`, `settings`, `braces`. An unknown name renders `modules` silently, so check the list.
78
87
 
79
88
  ## 5. Classes a module writes by hand (`packages/ui/src/styles/components.css`)
80
89
 
81
- Layout `ui-view`, `ui-two-col` (+`--wide-aside`), `ui-grid-2`, `ui-kpi-grid`, `ui-tag-cloud`, `ui-section-head` (h2 plus actions inside a view), `ui-toolbar` (+`__spacer`). Surfaces `ui-card` (+`__head`, `__title`, `__body`). Data `ui-table` (+`ui-table-wrap`, `ui-table__empty`, `ui-table__state` for a dot plus label, `.num`), `ui-cell` (+`ui-cell__muted`), `ui-mono`, `ui-code`, `ui-dot` (+`--muted`). Row action classes are component-owned and are never written by a module. Forms `ui-form` (+`__row`, `__row--4`, `__foot`, `__actions`), `ui-input` (+`--error`), `ui-select`, `ui-textarea` (+`--error`), `ui-checkbox`, `ui-label`, `ui-help` (+`--error`). Drawer `ui-drawer__form`, `ui-drawer__body`, `ui-drawer__foot`. Bits `ui-kbd`, `ui-note`, `ui-menu` (+`__label`, `__item`, `__item--active`, `__item--danger`, `__sep`), `ui-btn ui-btn--icon` for an icon-only button. Classes rendered by components (`ui-drawer__panel`, `ui-search`, `ui-page-head*`, `ui-field`, `ui-empty*`, `ui-alert*`, `ui-tag*`, `ui-kpi__*`, `ui-checks*`, `ui-avatar*`) are not written by hand.
90
+ Layout `ui-view`, `ui-two-col` (+`--wide-aside`), `ui-grid-2`, `ui-kpi-grid`, `ui-tag-cloud`, `ui-section-head` (h2 plus actions inside a view), `ui-toolbar` (+`__spacer`). Surfaces `ui-card` (+`__head`, `__title`, `__body`). Data `ui-table` (+`ui-table-wrap`, `ui-table__empty`, `ui-table__state` for a dot plus label, `.num`), `ui-cell` (+`ui-cell__muted`), `ui-mono`, `ui-code`, `ui-dot` (+`--muted`). Row action classes are component-owned and are never written by a module. Forms `ui-form` (+`__row`, `__row--4`, `__foot`, `__actions`), `ui-input` (+`--error`), `ui-select`, `ui-textarea` (+`--error`), `ui-checkbox`, `ui-label`, `ui-help` (+`--error`). Drawer `ui-drawer__form`, `ui-drawer__body`, `ui-drawer__foot`. Bits `ui-kbd`, `ui-note`, `ui-menu` (+`__label`, `__item`, `__item--active`, `__item--danger`, `__sep`), `ui-btn ui-btn--icon` for an icon-only button. Classes rendered by components (`ui-drawer__panel`, `ui-search`, `ui-page-head*`, `ui-field`, `ui-empty*`, `ui-alert*`, `ui-tag*`, `ui-kpi__*`, `ui-checks*`, `ui-avatar*`, `ui-table__sort`, `ui-pagination` (+`__summary`, `__size`, `__pages`), `ui-datefield` (+`__reading`), `ui-daterange` (+`__row`), `ui-tabs` (+`__tab`), `ui-toasts` with `ui-toast`) are not written by hand.
82
91
 
83
92
  ## 6. Copy
84
93
 
85
94
  User-facing copy lives in every declared `translations/*.json` bundle and is read with fully qualified `t()` keys. Eyebrow names the domain, title names the records, and description is one sentence. Table headers say what the value is. Buttons start with a verb. Loading text ends with `…`. Drawer footer states the constraint the user cannot see. Write natural copy in each locale, with no exclamation marks or database jargon.
86
95
 
96
+ ## 7. Inspect the rendered screen
97
+
98
+ A screen is not finished until it has been looked at. Typecheck and tests say nothing about overflow, alignment, a duplicate label or a column that collapses.
99
+
100
+ Where to look:
101
+
102
+ - Repository root: `pnpm dev`, then `http://localhost:4310`. `pnpm flowdular setup quick --apply --confirm reset-local-auth` (stop `pnpm dev` first; local only) seeds two demo tenants and two logins: `admin@example.com` / `Owner!23456789` owns both tenants, `user@example.com` / `Member!2345678` is a reduced-scope member. Navigate to the entry's navigation group and open the view.
103
+ - Sandbox: the session preview panel renders the draft module. Use it; a specialist has no shell and no dev server.
104
+ - No browser at hand: a headless Chrome screenshot (`--headless --window-size=1440,900 --screenshot=<file>`) captures what a fresh, signed-out session sees, which covers sign-in and public pages only. A protected screen needs a real session, so drive it with whatever browser automation the host offers rather than a bare screenshot flag.
105
+
106
+ Record one piece of evidence per state before handing off. A state you could not reach is stated as such, not assumed:
107
+
108
+ | State | How to reach it | What to check |
109
+ | --------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
110
+ | Loading | First paint before the fetch resolves, or a throttled network profile | Column widths match the populated table, the head does not jump, a refresh keeps rows |
111
+ | Empty | A tenant with no records | The icon renders, the sentence names the first action, `emptyFiltered` differs |
112
+ | Error | Make the request fail (sign out in a second tab, or drop the scope) | `Alert` under the header, no blank screen, no raw stack or SQL in the message |
113
+ | Populated | Several real records, including the longest realistic value | No horizontal page scroll, identifiers in `ui-mono`, numbers tabular, actions aligned |
114
+ | Denied | Sign in as `user@example.com` | The manage action is absent, navigation is hidden, a forced request still returns 403 |
115
+
116
+ Check the drawer form in the same pass: one label per field, fields top-aligned, the footer constraint visible, the submit button disabled while busy.
117
+
87
118
  ## Pitfalls
88
119
 
89
120
  - `Kpi value={items.length}` does not typecheck; use `String(items.length)`.
@@ -6,6 +6,7 @@ Flowdular is an agentic foundation framework. The platform under `packages/`, `m
6
6
 
7
7
  | Path | Consumer |
8
8
  | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `platform-capabilities.md` | The closed capability card: what the platform provides and what it does not. `spec-interview` and both module skills read it instead of scanning `modules/` and `packages/`. `pnpm capabilities:check` (`scripts/platform-capabilities.mjs`, run by `pnpm verify`) fails when its UI export, navigation group or workspace slot lists drift from the code. |
9
10
  | `rules/*.md` | Canonical cross-agent instructions. RuleSync generates the root `AGENTS.md` and `CLAUDE.md` from these files; `pnpm rules:check` rejects drift. |
10
11
  | `agents/sandbox/*.md` | Loaded at sandbox start by `packages/coding-agent/src/roles/registry.ts` (`loadAgentRoles`); `gates`, `handoff` and `allowedPaths` are enforced. `dependencies` always runs, a `HANDOFF:` line must name a role from the list and never the role itself, and writes outside the active role allowlist are quarantined and restored before validation. Defaults in `packages/coding-agent/src/roles/defaults.ts` are regenerated from these files by `pnpm --filter @flowdular/coding-agent sync-roles`, and `tests/sync.test.ts` fails when they drift. |
11
12
  | `agents/{module-executor,reviewer,spec-author}.md` | Read by people and coding tools at the repository root; named in `blueprints/*/blueprint.json`. Not loaded by code. |
@@ -13,7 +14,7 @@ Flowdular is an agentic foundation framework. The platform under `packages/`, `m
13
14
  | `blueprints/*/blueprint.json` | `pnpm flowdular blueprint list` and `blueprint validate --all` (`packages/cli/src/runner.ts`, discovery in `packages/cli/src/validation.ts` `findNamedFiles`) validate every `blueprint.json` against `packages/contracts/schemas/blueprint.schema.json` and check the companion files exist; `pnpm validate` runs it in CI. The sandbox labels sessions `new-module@1.0.0` and `edit-module@1.0.0`. |
14
15
  | `blueprints/*/*.yaml`, `*.schema.json`, `examples/` | Existence-checked by `validateBlueprint`; otherwise documentation for agents and reviewers. Nothing executes `steps.yaml` or `gates.yaml`. |
15
16
  | `policies/capabilities.yaml`, `policies/model-routing.yaml` | Existence-checked by `pnpm flowdular doctor`. The real policy is code: `packages/cli/src/capabilities.ts`, `modules/*/src/cli/commands.json`, `packages/cli/src/runner.ts`, `packages/sandbox/src/server/planning.ts`. |
16
- | `policies/task-budgets.yaml`, `policies/path-ownership.yaml` | Review guidance only. |
17
+ | `policies/task-budgets.yaml`, `policies/path-ownership.yaml` | Read by the `git-pr` delivery target (`packages/sandbox/src/server/delivery/policies.ts`): the changed-file and new-dependency budget per session kind, and path owners plus `crossOwnerChanges.requireReviewer` for the pull request body. Review guidance otherwise. |
17
18
  | `examples/**` | Reference shapes for agents; not compiled or tested. |
18
19
 
19
20
  `AGENTS.md` and `docs/design-system.md` are copied into each session's `reference/` as well (`packages/sandbox/src/server/reference.ts`). `AGENTS.md`, `CLAUDE.md`, `.agents/skills` and `.claude/skills` are generated compatibility outputs. Edit `.ai/rules` or `.ai/skills`, then run `pnpm rules:generate`.
@@ -12,7 +12,11 @@ handoff:
12
12
  - ux-designer
13
13
  ---
14
14
 
15
- You own specification decisions and locale terminology, never implementation. Use only the Task skill selected under Session. Consult reference/packages/contracts/schemas/module-spec.schema.json and reference/example-module/spec/module.yaml when writing the spec.
15
+ You own specification decisions and locale terminology, never implementation. Use only the Task skill selected under Session. Consult reference/platform-capabilities.md, reference/packages/contracts/schemas/module-spec.schema.json and reference/example-module/spec/module.yaml when writing the spec.
16
+
17
+ Write schemaVersion 2: entities with typed fields and states, screens, actions, widgets, settings, agentTools, plus outOfScope and decisions. Fill decisions for every choice, including the platform defaults you proposed. The capability card is closed: anything it lists as missing goes to outOfScope with the business decision, never into a scenario. v1 specs stay valid.
18
+
19
+ When a decision is missing, end the reply with exactly one fenced block tagged questions holding {"questions":[{"id":"Q-1","question":"...","options":["..."],"recommended":"...","allowFreeText":true}]} and nothing after it. The operator answers in a form and the replies arrive next turn as a Decisions section.
16
20
 
17
21
  For an edit, compare against base/modules/<dir>/spec/module.yaml and make the smallest delta covering the brief. Start new specs as draft; change an existing approved spec to draft or in-review before editing requirements. Never set approved: only the operator records approval of the exact hash. Later edits invalidate it.
18
22
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  Turn an agreed business request into `modules/<dir>/spec/module.yaml`. In the sandbox this is the first turn of a `new-module` session, taken by `business-manager`; at the repository root it is `spec-author` following `.ai/agents/spec-author.md`. The blueprint creates or revises the specification only. It never creates implementation files and never sets `status: approved`; approval belongs to a human owner (the sandbox approve button, or an edit in the pull request).
4
4
 
5
- The result must validate against `packages/contracts/schemas/module-spec.schema.json` (`pnpm flowdular spec validate --all --json`) and be decision complete: permissions with one read and one manage id per entity, invariants for tenancy and uniqueness, data ownership, and acceptance scenarios that include a denial and a tenant isolation case. Unknown business decisions stay explicit questions in the handoff, never guesses. `templates/module.yaml` is a starting point with the keys the schema knows; `.ai/agents/sandbox/business-manager.md` carries a complete minimal example.
5
+ The result is a `schemaVersion: 2` document that validates against `packages/contracts/schemas/module-spec.schema.json` (`pnpm flowdular spec validate --all --json`, including the cross checks between actions, permissions, entities and fields) and is decision complete: permissions with one read and one manage id per entity, entities with typed fields and tenant uniqueness, screens and actions, invariants for tenancy and uniqueness, data ownership, acceptance scenarios that include a denial and a tenant isolation case, `outOfScope` for everything `.ai/platform-capabilities.md` lists as missing, and `decisions` recording every choice and who made it. The procedure is the `spec-interview` skill: propose a platform default per decision and ask only what cannot be inferred, through the questions protocol described in `.ai/agents/sandbox/business-manager.md`. Unknown business decisions stay explicit questions, never guesses. `templates/module.yaml` is a starting point with the keys the schema knows. Version 1 specs stay valid for existing modules.
@@ -1,5 +1,9 @@
1
1
  schemaVersion: 1
2
2
  # Keys below exist in packages/contracts/schemas/module-spec.schema.json.
3
+ # A new specification is authored at specSchemaVersion 2: it carries the domain
4
+ # model, so the implementing agent reads the spec instead of the repository.
5
+ # An existing version 1 specification stays valid and is not rewritten.
6
+ specSchemaVersion: 2
3
7
  status:
4
8
  initial: draft
5
9
  allowed:
@@ -22,14 +26,54 @@ requiredKeys:
22
26
  - permissions
23
27
  - dataOwnership
24
28
  - acceptanceScenarios
29
+ - decisions
30
+ - outOfScope
31
+ byCapability:
32
+ database:
33
+ entities: at least one, with every persisted field typed and one field unique inside the tenant
34
+ client:
35
+ screens: at least one list screen whose columns and filters are field ids of its entity
36
+ api:
37
+ actions: one per mutation, each naming a permission the spec declares
25
38
  identifiers:
26
39
  id: ^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$
27
40
  permissions: <module>.<entity>.read and <module>.<entity>.manage
28
41
  acceptanceScenarios: ^[A-Z][A-Z0-9-]+$
42
+ entities: ^[a-z][a-z0-9-]*$
43
+ fields: ^[a-z][a-zA-Z0-9]*$
44
+ screens, actions, widgets: ^[a-z][a-z0-9-]*$
45
+ settings: ^[a-z][a-zA-Z0-9]*$
46
+ agentTools: <module>.<entity>.<verb>, the dotted tool identity the registry uses
47
+ decisions: ^[A-Z][A-Z0-9-]+$
48
+ entities:
49
+ fieldTypes:
50
+ - string
51
+ - text
52
+ - integer
53
+ - decimal
54
+ - boolean
55
+ - date
56
+ - datetime
57
+ - enum
58
+ - reference
59
+ - json
60
+ rules:
61
+ - an enum field declares values; a reference field names an entity of this
62
+ spec or <moduleId>.<entityId> of a declared dependency
63
+ - 'the first entity drives the scaffold: fields become the domain type, the
64
+ 0001 migration columns, the repository mapping and the create input'
65
+ - states.field names the enum field that declares exactly states.values
66
+ - no field is called id, tenantId or createdAt; every tenant table owns them
67
+ - no field is a PostgreSQL reserved word such as order, user, group or end
29
68
  acceptanceScenarios:
30
69
  minimum: 3
31
70
  mustInclude:
32
71
  - denial
33
72
  - tenant isolation
73
+ decisions:
74
+ policy: every answered question is recorded, decidedBy user when the operator
75
+ answered and default when a platform default was applied instead
76
+ outOfScope:
77
+ policy: what the business excluded, and what the platform does not provide yet
34
78
  openQuestions:
35
79
  policy: stay in the handoff message; never guessed into the file
@@ -2,17 +2,17 @@ schemaVersion: 1
2
2
  steps:
3
3
  - id: collect
4
4
  role: business-manager
5
- action: Validate the brief against input.schema.json (module id, outcome, actors); list the business facts still missing.
6
- inputs: [brief]
7
- outputs: [open questions]
5
+ action: Validate the brief against input.schema.json (module id, outcome, actors); walk the spec-interview decision checklist against .ai/platform-capabilities.md, propose a platform default for every decision and list the ones that cannot be inferred.
6
+ inputs: [brief, platform-capabilities.md]
7
+ outputs: [proposed decisions, open questions]
8
8
  - id: ask
9
9
  role: business-manager
10
- action: Stop and ask for every unresolved business or security decision instead of guessing.
10
+ action: Ask every unresolved decision through the questions protocol (one fenced questions block in the sandbox, the question tool in Claude Code) instead of guessing; each answer becomes a decisions[] entry.
11
11
  inputs: [open questions]
12
12
  outputs: [answers]
13
13
  - id: draft
14
14
  role: business-manager
15
- action: Write spec/module.yaml from templates/module.yaml with status draft; permissions, invariants, dataOwnership, acceptanceScenarios complete.
15
+ action: Write spec/module.yaml (schemaVersion 2) from templates/module.yaml with status draft; permissions, invariants, dataOwnership, acceptanceScenarios, entities with typed fields, screens, actions, outOfScope and decisions complete.
16
16
  inputs: [answers]
17
17
  outputs: [spec/module.yaml]
18
18
  gates: [spec-schema]