create-flowdular 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.agents/skills/deploy-operate/SKILL.md +109 -0
- package/agent-template/.agents/skills/module-new/SKILL.md +29 -0
- package/agent-template/.agents/skills/module-update/SKILL.md +9 -1
- package/agent-template/.agents/skills/spec-interview/SKILL.md +114 -0
- package/agent-template/.agents/skills/ux-design/SKILL.md +34 -3
- package/agent-template/.ai/README.md +2 -1
- package/agent-template/.ai/agents/sandbox/business-manager.md +5 -1
- package/agent-template/.ai/blueprints/author-spec/README.md +1 -1
- package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
- package/agent-template/.ai/blueprints/author-spec/steps.yaml +5 -5
- package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +99 -3
- package/agent-template/.ai/blueprints/edit-module/gates.yaml +4 -0
- package/agent-template/.ai/blueprints/edit-module/required-files.yaml +9 -0
- package/agent-template/.ai/blueprints/new-module/gates.yaml +4 -0
- package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
- package/agent-template/.ai/blueprints/release/gates.yaml +4 -0
- package/agent-template/.ai/platform-capabilities.md +128 -0
- package/agent-template/.ai/policies/capabilities.yaml +100 -0
- package/agent-template/.ai/policies/path-ownership.yaml +5 -2
- package/agent-template/.ai/policies/task-budgets.yaml +5 -3
- package/agent-template/.ai/references/catalog/module.json +4 -4
- package/agent-template/.ai/references/catalog/package.json +2 -2
- package/agent-template/.ai/references/catalog/spec/module.yaml +5 -3
- package/agent-template/.ai/references/catalog/src/platform.ts +2 -0
- package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +89 -1
- package/agent-template/.ai/references/catalog/src/services/data-classes.ts +47 -0
- package/agent-template/.ai/references/catalog/src/services/database-repository.ts +98 -1
- package/agent-template/.ai/references/catalog/src/services/repository.ts +22 -1
- package/agent-template/.ai/references/catalog/tests/data-classes.test.ts +157 -0
- package/agent-template/.ai/references/catalog.provenance.json +12 -10
- package/agent-template/.ai/rules/flowdular.md +4 -0
- package/agent-template/.ai/skills/README.md +10 -0
- package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -2
- package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.ai/skills/business-agent-design/SKILL.md +0 -1
- package/agent-template/.ai/skills/deploy-operate/SKILL.md +114 -0
- package/agent-template/.ai/skills/module-new/SKILL.md +29 -3
- package/agent-template/.ai/skills/module-update/SKILL.md +9 -3
- package/agent-template/.ai/skills/perf-audit/SKILL.md +0 -1
- package/agent-template/.ai/skills/release-eject-pr/SKILL.md +0 -1
- package/agent-template/.ai/skills/spec-interview/SKILL.md +120 -0
- package/agent-template/.ai/skills/test-hardening/SKILL.md +1 -0
- package/agent-template/.ai/skills/ux-design/SKILL.md +34 -3
- package/agent-template/.ai/skills/variables/SKILL.md +0 -2
- package/agent-template/.ai/skills/workflow-development/SKILL.md +0 -1
- package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.claude/skills/deploy-operate/SKILL.md +109 -0
- package/agent-template/.claude/skills/module-new/SKILL.md +29 -0
- package/agent-template/.claude/skills/module-update/SKILL.md +9 -1
- package/agent-template/.claude/skills/spec-interview/SKILL.md +114 -0
- package/agent-template/.claude/skills/ux-design/SKILL.md +34 -3
- package/agent-template/AGENTS.md +4 -0
- package/agent-template/CLAUDE.md +4 -0
- package/agent-template/docs/adr/0003-module-settings.md +1 -1
- package/agent-template/docs/adr/0006-agentic-workflows.md +24 -21
- package/agent-template/docs/agent-contract.md +2 -2
- package/agent-template/docs/cli-extensions.md +82 -0
- package/agent-template/docs/cli.md +190 -0
- package/agent-template/docs/configuration.md +593 -36
- package/agent-template/docs/design-system.md +185 -31
- package/agent-template/docs/getting-started.md +118 -0
- package/agent-template/docs/module-distribution.md +96 -0
- package/agent-template/docs/module-web-surfaces.md +221 -0
- package/agent-template/docs/modules.md +216 -0
- package/agent-template/docs/operations.md +464 -0
- package/agent-template/docs/sandbox.md +212 -0
- package/agent-template/platform/scripts/build.mjs +10 -0
- package/dist/bin.js +65 -1
- package/package.json +1 -1
- package/template/default/.dockerignore +14 -0
- package/template/default/.env.example +94 -0
- package/template/default/README.md +37 -1
- package/template/default/flowdular.json +15 -4
- package/template/default/infra/README.md +116 -0
- package/template/default/infra/docker/Dockerfile +37 -0
- package/template/default/infra/docker/compose.yaml +158 -0
- package/template/default/infra/docker/postgres/10-roles.sh +31 -0
- package/template/default/infra/docker/postgres/tls-init.sh +28 -0
- package/template/default/infra/kubernetes/database-secret.example.yaml +15 -0
- package/template/default/infra/kubernetes/deployment.yaml +211 -0
- package/template/default/infra/kubernetes/kustomization.yaml +9 -0
- package/template/default/infra/kubernetes/secrets.example.yaml +52 -0
- package/template/default/infra/kubernetes/service.yaml +13 -0
- package/template/default/modules/example/module.json +2 -1
- package/template/default/modules/example/package.json +2 -2
- package/template/default/modules/example/spec/module.yaml +1 -1
- package/template/default/modules/example/src/services/database-repository.ts +2 -12
- package/template/default/package.json +3 -2
- package/template/default/platform/octane.config.ts +99 -9
- package/template/default/platform/package.json +1 -1
- package/template/default/platform/src/generated/modules.client.ts +26 -2
- package/template/default/platform/src/generated/modules.server.ts +241 -10
- package/template/default/platform/src/server/health.ts +47 -0
- package/template/default/platform/src/server/metrics.ts +100 -0
- package/template/default/platform/src/server/storage.ts +172 -0
- package/template/default/platform/src/server/tracing.ts +85 -0
- package/template/default/pnpm-workspace.yaml +1 -0
- package/template/default/specs/application.yaml +15 -0
|
@@ -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
|
|
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)`.
|
package/agent-template/AGENTS.md
CHANGED
|
@@ -54,6 +54,10 @@ Do not load the whole skill catalog into the task context.
|
|
|
54
54
|
12. Keep handoffs short and factual. No AI attribution footers or em/en dashes.
|
|
55
55
|
Sandbox final line: `HANDOFF: <allowed-role> - <why>` or
|
|
56
56
|
`HANDOFF: none - <why>`, never your own role.
|
|
57
|
+
13. Flow: request, `spec-interview`, approval, `module-new`/`module-update`,
|
|
58
|
+
`auto-review`. Implement from the approved spec and its touch list; do not
|
|
59
|
+
scan `modules/` or `packages/`. First lookup is `.ai/platform-capabilities.md`.
|
|
60
|
+
What the spec lacks is a spec defect to report, never a guess.
|
|
57
61
|
|
|
58
62
|
Detailed recipes: `docs/agent-contract.md` (lookup only).
|
|
59
63
|
Reference module: `.ai/references/catalog`; visual contract: `docs/design-system.md`.
|
package/agent-template/CLAUDE.md
CHANGED
|
@@ -54,6 +54,10 @@ Do not load the whole skill catalog into the task context.
|
|
|
54
54
|
12. Keep handoffs short and factual. No AI attribution footers or em/en dashes.
|
|
55
55
|
Sandbox final line: `HANDOFF: <allowed-role> - <why>` or
|
|
56
56
|
`HANDOFF: none - <why>`, never your own role.
|
|
57
|
+
13. Flow: request, `spec-interview`, approval, `module-new`/`module-update`,
|
|
58
|
+
`auto-review`. Implement from the approved spec and its touch list; do not
|
|
59
|
+
scan `modules/` or `packages/`. First lookup is `.ai/platform-capabilities.md`.
|
|
60
|
+
What the spec lacks is a spec defect to report, never a guess.
|
|
57
61
|
|
|
58
62
|
Detailed recipes: `docs/agent-contract.md` (lookup only).
|
|
59
63
|
Reference module: `.ai/references/catalog`; visual contract: `docs/design-system.md`.
|
|
@@ -13,7 +13,7 @@ The first setting is `auth.core.allowSignUp`. The server is authoritative and re
|
|
|
13
13
|
|
|
14
14
|
## Runtime (amendment, 2026-09-01)
|
|
15
15
|
|
|
16
|
-
- `@flowdular/sdk/kernel` exports `ModuleSettingsRuntime` (`declare`, `get`, `list`, `set`, `onChange`) and `createModuleSettingsRuntime(store)`. Values live in `module_settings` in auth.db (`tenant_id`, `module_id`, `key`, `value_json`, `updated_at`, `updated_by`); `auth.core` provides the store and exposes the runtime as `authRuntime.moduleSettings`.
|
|
16
|
+
- `@flowdular/sdk/kernel` exports `ModuleSettingsRuntime` (`declare`, `prime`, `get`, `list`, `set`, `onChange`) and `createModuleSettingsRuntime(store)`. Since RFC 0005 (2026-09-13) the store contract is asynchronous (`load`, `save` and `clear` return promises), `prime(tenantId)` loads a tenant's snapshot once, `get` and `list` read that snapshot synchronously and fail for an unprimed tenant, and `set` awaits the store. Values live in `module_settings` in auth.db (`tenant_id`, `module_id`, `key`, `value_json`, `updated_at`, `updated_by`); `auth.core` provides the store and exposes the runtime as `authRuntime.moduleSettings`.
|
|
17
17
|
- A setting is tenant-scoped by default. `scope: 'platform'` stores one value for the whole deployment (tenant id `''`); auth uses it for knobs that apply before a tenant is known (sign-up, session policy, password length, sign-in providers).
|
|
18
18
|
- Environment variables only supply the declared default. A stored value wins at read time; reads are live, never snapshotted at boot.
|
|
19
19
|
- A module declares settings by returning `settings` from `createServerComposition`; the platform registers every declaration after composing, then calls each composition's `start()`.
|
|
@@ -200,9 +200,11 @@ substitute the run id as if it were an agent identity.
|
|
|
200
200
|
|
|
201
201
|
### Automations triggers a workflow
|
|
202
202
|
|
|
203
|
-
Version one
|
|
204
|
-
|
|
205
|
-
maps a schedule or signed webhook to the execution
|
|
203
|
+
Version one keeps the coupling inside `automations.core`: it declares
|
|
204
|
+
`workflows.core` as a dependency, registers a workflow target adapter in its own
|
|
205
|
+
target registry, and maps a schedule or signed webhook to the execution
|
|
206
|
+
capability. The adapter resolves the capability per call, so a workspace may
|
|
207
|
+
still leave `workflows.core` uninstalled.
|
|
206
208
|
|
|
207
209
|
```ts
|
|
208
210
|
await workflows.enqueue(
|
|
@@ -1044,18 +1046,18 @@ Run history is tenant-scoped and cursor-paginated. It supports filters for:
|
|
|
1044
1046
|
- child agent id;
|
|
1045
1047
|
- failure or refusal code.
|
|
1046
1048
|
|
|
1047
|
-
Interactive ordering is
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
filter
|
|
1055
|
-
`
|
|
1056
|
-
|
|
1057
|
-
Workflow audit pages use the same
|
|
1058
|
-
`
|
|
1049
|
+
Interactive ordering is a keyset over `(queuedAt, runId)` in one direction,
|
|
1050
|
+
descending by default. Since RFC 0005 the cursor is the platform's signed
|
|
1051
|
+
cursor (`encodeCursor` in `@flowdular/sdk/server`, keyed by
|
|
1052
|
+
`FD_WORKFLOWS_CURSOR_KEY` with the previous key still verifying) carrying the
|
|
1053
|
+
tenant id, the sort, the direction, a canonical filter digest and the last
|
|
1054
|
+
`(queuedAt, runId)` pair; the keyset order keeps forward pages stable without
|
|
1055
|
+
a snapshot boundary. A cursor is valid only for the authenticated tenant and
|
|
1056
|
+
the exact sort and filter set that created it. Malformed, modified,
|
|
1057
|
+
foreign-tenant or mismatched cursors return `CURSOR_INVALID` and no rows.
|
|
1058
|
+
|
|
1059
|
+
Workflow audit pages use the same cursor over `(sequence)` or
|
|
1060
|
+
`(occurredAt, sequence)`. Run detail is not cursor-paged because contract limits bound it to at
|
|
1059
1061
|
most 100 nodes, 500 immutable attempts, and 200 edge settlements. The event
|
|
1060
1062
|
stream remains separately paged because it may contain 10,000 events.
|
|
1061
1063
|
|
|
@@ -1085,12 +1087,13 @@ and unrestricted request bodies are never history data.
|
|
|
1085
1087
|
### Event stream and resume
|
|
1086
1088
|
|
|
1087
1089
|
Every run event has a run-local sequence and durable event id. SSE `id` is an
|
|
1088
|
-
opaque run-bound resume cursor
|
|
1089
|
-
|
|
1090
|
-
|
|
1090
|
+
opaque run-bound resume cursor, the shared signed cursor of `@flowdular/sdk/server`
|
|
1091
|
+
carrying kind `events` over tenant, run, and sequence, signed with the module
|
|
1092
|
+
cursor key. It is not the database event id. The event data still includes
|
|
1093
|
+
`eventId`, `schemaVersion`, and `sequence`.
|
|
1091
1094
|
|
|
1092
|
-
A client reconnects with either HTTP `Last-Event-ID: <
|
|
1093
|
-
integer `afterSequence`. If both are supplied they must name the same run and
|
|
1095
|
+
A client reconnects with either HTTP `Last-Event-ID: <signed events cursor>` or
|
|
1096
|
+
an integer `afterSequence`. If both are supplied they must name the same run and
|
|
1094
1097
|
sequence or the server returns `WORKFLOW_EVENT_CURSOR_CONFLICT`. A cursor for a
|
|
1095
1098
|
different tenant or run returns `WORKFLOW_EVENT_CURSOR_INVALID`. A sequence
|
|
1096
1099
|
beyond the durable tail returns `WORKFLOW_EVENT_CURSOR_AHEAD`.
|
|
@@ -1230,7 +1233,7 @@ history cannot share a database transaction; the workflow atomically commits
|
|
|
1230
1233
|
their stable correlation identifiers and each owner keeps its own audit
|
|
1231
1234
|
boundary.
|
|
1232
1235
|
|
|
1233
|
-
Audit pagination uses tenant sequence
|
|
1236
|
+
Audit pagination uses the tenant sequence and the shared signed cursor. Verify
|
|
1234
1237
|
returns a typed result with `valid`, `checkedThroughSequence`, and, when broken,
|
|
1235
1238
|
`firstBrokenSequence`, `expectedPreviousHash`, and `actualPreviousHash`. It never
|
|
1236
1239
|
returns secret metadata.
|
|
@@ -31,8 +31,8 @@ Use the already selected task skill. Consult `.ai/references/catalog` for implem
|
|
|
31
31
|
20. Passwords, session tokens and provider credentials never leave `auth.core` (or the `agents.core` vault) and never appear in logs, audit metadata or responses.
|
|
32
32
|
21. `setup quick` and `auth greenfield` are destructive local resets. Preview first, stop the app, never point them at a custom or deployed database.
|
|
33
33
|
22. Agent workers reach ERP data only through registered API or CLI tools. A module defines them with `defineApiAgentTool` or `defineCliAgentTool` from `@flowdular/sdk/harness/tool-adapters` and registers them inside `createServerComposition` with `context.agentTools.register([...])`; nothing else registers a tool. A module may also ship business agents with `defineAgent()` from `@flowdular/sdk/modules/agents/server` and `context.agentDefinitions.register([...])` after declaring an `agents.core` dependency. Their code allowlist is only a maximum: effective tools are the exact intersection of that allowlist, the tenant binding reduction, invocation grants, registered tool permissions, the actor's saved permission ceiling, and live authorization. Sandbox coding specialists are not business agents.
|
|
34
|
-
23. A background run is persisted before enqueue returns; idempotency, leases, recovery, tenant scoping and append-only audit evidence for every fire-and-forget run.
|
|
35
|
-
24. Module settings are declared with `defineModuleSettings` from `@flowdular/sdk/kernel`, returned as `settings` from the composition, read
|
|
34
|
+
23. A background run is persisted before enqueue returns; idempotency, leases, recovery, tenant scoping and append-only audit evidence for every fire-and-forget run. A poll loop is `createJobRunner` from `@flowdular/sdk/server`, never a hand-written `setInterval`: it owns the timer and its unref, the guard against overlapping passes, the bound on claims per pass, per-item isolation, the lease renewal and its `CLAIM_LOST` abort, the backoff and the drain, while the module keeps its table, its routing read, its claim and renewal statements and its stale window. `packages/server/src/jobs/index.ts` carries the adoption recipe per module.
|
|
35
|
+
24. Module settings are declared with `defineModuleSettings` from `@flowdular/sdk/kernel`, returned as `settings` from the composition, read with `context.settings.get(tenantId, '<module>.core', key)` after the tenant was primed (`await context.settings.prime(tenantId)`; the authenticated request path and the platform composition prime, a background path primes itself, an unprimed read fails loudly), written with `await context.settings.set(...)` against an asynchronous store, and rendered in that module's drawer under Administration, Modules. User-facing setting copy declares fully qualified `labelKey` and `descriptionKey` present in every module locale, with English `label` and `description` literals as compatibility fallbacks. Values, identifiers and secrets are never translated. Administration, Settings is only for workspace and organization settings. A cross-module setting read needs a declared dependency and a `shared`, non-secret setting. Cross-module operations use a typed public service registered in `context.capabilities` by the owning module and resolved by a declared dependent, never the other module's database.
|
|
36
36
|
25. `Development` navigation is owner-only in the client; server permissions remain authoritative.
|
|
37
37
|
26. Generated files are production code: no placeholders, silent fallbacks or skipped tests. `module.json` `version`, spec `specVersion` and `package.json` `version` move together.
|
|
38
38
|
27. Commits and pull requests: short body, one line on verification, no AI attribution footers, generated files only through the CLI. Never use em or en dashes in anything you write.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Module CLI extensions
|
|
2
|
+
|
|
3
|
+
The core CLI is the controlled automation boundary for developers, agents, and CI. An enabled ERP module may add commands inside its own namespace. A `customer.core` module can provide `customer export`, but it cannot claim `module`, `spec`, or another core command group.
|
|
4
|
+
|
|
5
|
+
## Contract
|
|
6
|
+
|
|
7
|
+
A module with the `cli` capability declares two paths in `module.json`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"capabilities": ["cli"],
|
|
12
|
+
"cli": {
|
|
13
|
+
"catalog": "src/cli/commands.json",
|
|
14
|
+
"entry": "src/cli/index.ts"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`commands.json` is the discovery and policy surface. It is schema-validated without executing module code:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"protocolVersion": 1,
|
|
24
|
+
"moduleId": "customer.core",
|
|
25
|
+
"commands": [
|
|
26
|
+
{
|
|
27
|
+
"path": ["customer", "export"],
|
|
28
|
+
"capability": {
|
|
29
|
+
"id": "customer.export",
|
|
30
|
+
"version": 1,
|
|
31
|
+
"summary": "Export tenant-scoped customer records.",
|
|
32
|
+
"risk": "workspace-write",
|
|
33
|
+
"requiresApprovedSpec": true,
|
|
34
|
+
"supportsDryRun": true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The implementation exports the matching versioned command:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { defineCliExtension } from '@flowdular/sdk/cli-protocol';
|
|
45
|
+
|
|
46
|
+
export default defineCliExtension({
|
|
47
|
+
protocolVersion: 1,
|
|
48
|
+
moduleId: 'customer.core',
|
|
49
|
+
commands: [
|
|
50
|
+
{
|
|
51
|
+
path: ['customer', 'export'],
|
|
52
|
+
capability: {
|
|
53
|
+
id: 'customer.export',
|
|
54
|
+
version: 1,
|
|
55
|
+
summary: 'Export tenant-scoped customer records.',
|
|
56
|
+
risk: 'workspace-write',
|
|
57
|
+
requiresApprovedSpec: true,
|
|
58
|
+
supportsDryRun: true,
|
|
59
|
+
},
|
|
60
|
+
execute: ({ apply, arguments: args }) => ({
|
|
61
|
+
data: { applied: apply, format: args[0] ?? 'json' },
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The CLI imports this code only after the exact command or capability is invoked. It rejects catalog and implementation drift before calling the handler.
|
|
69
|
+
|
|
70
|
+
## Execution rules
|
|
71
|
+
|
|
72
|
+
- Command paths and capability IDs must start with the first segment of the module ID.
|
|
73
|
+
- The module must be enabled in `flowdular.json`.
|
|
74
|
+
- Catalog and entry paths must resolve inside the module, including through symlinks.
|
|
75
|
+
- A command marked `requiresApprovedSpec` requires `--spec <path>` and an approved, schema-valid spec.
|
|
76
|
+
- Non-read commands are dry-run by default when supported. Other non-read commands require `--apply`.
|
|
77
|
+
- External and non-local destructive module capabilities remain disabled until a signed approval verifier is configured.
|
|
78
|
+
- A workspace-local destructive capability must declare `localOnly` and a typed confirmation token. It remains dry-run unless both `--apply` and the exact `--confirm` value are present, and it is blocked outside development and test.
|
|
79
|
+
- `capability list`, `capability describe`, and `capability run` use the same descriptors and handlers as direct commands.
|
|
80
|
+
- Core commands may reuse an extension: `module enable <id> --apply` runs the `auth.scopes.sync` capability of `auth.core` after regenerating the composition, so a freshly enabled module is visible to workspace owners without a second command.
|
|
81
|
+
|
|
82
|
+
The complete customer example is in `.ai/examples/customer-cli-extension`. New module scaffolds include the catalog and implementation files when the approved spec declares the `cli` capability.
|