dreamteamer 0.7.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "A workspace compiler for coding agents \u2014 schema-validated records as plain files over git, compiled into every harness",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Gilad Khen <giladkhen@gmail.com>",
@@ -62,7 +62,7 @@ These were duplicated across seven skills; they are true for all of them.
62
62
  conversation already in progress. A new command, agent or skill is available in the **next**
63
63
  session. Say so rather than letting the operator wonder.
64
64
  6. **References are qualified** — `skills/<id>`, `agents/<id>`, `commands/<id>`, `collections/<id>`,
65
- `users/<id>`. A bare name fails `check`.
65
+ and `<collection>/<id>` for any record. A bare name fails `check`.
66
66
  7. **Never edit generated output.** `.dreamteamer/`, `.claude/`, `.agents/`, `.cursor/` are all
67
67
  overwritten and pruned on the next compile. If you found the thing you want to change in one of
68
68
  those, you are in the wrong file.
@@ -73,9 +73,11 @@ These were duplicated across seven skills; they are true for all of them.
73
73
  9. **Never duplicate a procedure across records.** A command body that restates a skill, an agent
74
74
  body that inlines its skill's steps, a command that re-types another command's prompt — each is two
75
75
  copies that drift. Reference the one that owns it.
76
- 10. **Module-shipped entities must not name a workspace's own users, accounts or paths.** Use
77
- `@initiator` / `@me`, and read per-install values from `.env` naming the variable. A hard-coded
78
- `users/<someone>` does not resolve in anyone else's workspace.
76
+ 10. **Module-shipped entities must not name a workspace's own people, accounts or paths.** Read
77
+ per-install values from `.env` naming the variable, and leave who-did-what to a collection the
78
+ workspace owns. A hard-coded `contacts/<someone>` does not resolve in anyone else's workspace.
79
+ ⚠ **There is no `@me` since 0.8.0** — it expanded to `users/<slug>`, and `users` is gone. A
80
+ ui-view filter still using it is a compile error, not a view that quietly shows nothing.
79
81
 
80
82
  ## the loop
81
83
 
@@ -97,11 +99,12 @@ a collection about people, meetings, tasks, products, content — belongs in a m
97
99
  version of it belongs in the `recipes` repo rather than here.
98
100
 
99
101
  **The test is: does the ENGINE read it?** Core's collections are the entity kinds the compiler itself
100
- materializes, plus `users` (because `@me` resolves against it) and `repos` (because `repos ensure`
101
- clones them). Everything else has been ejected on exactly that test — `teams` (nothing resolved a
102
+ materializes, plus `repos` (because `repos ensure` clones them). Everything else has been ejected on
103
+ exactly that test — `teams` (nothing resolved a
102
104
  team), `mounts` (a one-implementation adapter enum over an `.env` key), `module-registries` (zero
103
105
  readers), `workflows`/`workflow-runs`/`workflow-triggers`/`cursors` and `migrations`/`migration-runs`
104
- (measured unused), and finally `tasks`, whose only claim to core had been the workflow gate that no
106
+ (measured unused), `users` (0.8.0 its justification was circular: core because `@me` resolved
107
+ against it, and `@me` existed because it was core), and finally `tasks`, whose only claim to core had been the workflow gate that no
105
108
  longer exists. `npm run metrics` in the engine holds the budgets that keep this honest.
106
109
 
107
110
  ## common mistakes
@@ -13,7 +13,7 @@ description: triage every open task assigned to me, one at a time
13
13
  argument-hint: "[assignee]"
14
14
  ---
15
15
  load this workspace's tasks skill. list my open tasks
16
- (`npm run --silent dt -- tasks list --assignee users/<me> --status todo`), then walk them one at a
16
+ (`npm run --silent dt -- tasks list --status todo`), then walk them one at a
17
17
  time: restate it, ask me to keep / reassign / drop, apply the decision with `tasks set`.
18
18
  done when the list is empty or I say stop.
19
19
  ```
@@ -5,7 +5,7 @@ route plus the id of an already-registered layout, plus how to shape the data. N
5
5
 
6
6
  The surface reads compiled ui-view records at boot: `nav` becomes a sidebar entry, `path` becomes a
7
7
  live route, `target: list` renders the named `layout` over the collection with `filter`/`options`
8
- applied (`@me` resolves to the current operator). After authoring: compile, then reload the surface.
8
+ applied. After authoring: compile, then reload the surface.
9
9
 
10
10
  ```yaml
11
11
  path: /inbox
@@ -13,10 +13,14 @@ nav: { label: Inbox, icon: inbox, order: 1 }
13
13
  target: list
14
14
  collection: collections/tasks
15
15
  layout: table
16
- filter: { assignee: { _eq: "@me" } } # operator objects, never a bare value
17
- options: { columns: [name, status, due, run], sort: -due }
16
+ filter: { status: { _eq: todo } } # operator objects, never a bare value
17
+ options: { columns: [name, status, due], sort: -due }
18
18
  ```
19
19
 
20
+ ⚠ **`@me` no longer exists** — it was removed with the `users` collection in 0.8.0, and a filter using
21
+ it is a compile error rather than a view that quietly shows nothing. Filter on a field this workspace
22
+ owns; where a person is genuinely the axis, the workspace ships its own collection of people.
23
+
20
24
  | field | required | notes |
21
25
  |---|---|---|
22
26
  | `path` | yes | the route — `/inbox`, `/views/meetings/recent` |
@@ -53,7 +57,8 @@ through the same compile gate.
53
57
  |---|---|
54
58
  | a `layout` id you assumed exists | compile validates it only for `target: list`; otherwise the view renders nothing |
55
59
  | bare `collection: tasks` | qualified refs only |
56
- | `filter: { assignee: "@me" }` | filters are operator objects: `{ assignee: { _eq: "@me" } }` |
60
+ | `filter: { status: "todo" }` | filters are operator objects: `{ status: { _eq: todo } }` |
61
+ | a filter using `@me` | gone in 0.8.0 with `users` — compile refuses it by name |
57
62
  | a column the schema does not have | dropped silently — the row loses that value with no error |
58
63
  | a ui-view that restates the built-in fallback | a record to maintain for zero gain |
59
- | a module ui-view filtered to a named user | use `@me`; a hard-coded id breaks elsewhere |
64
+ | a module ui-view naming one person | a hard-coded id resolves in no other workspace |
@@ -63,8 +63,8 @@ when you're not sure the runtime is fresh.
63
63
  | the workspace lacks the capability entirely | `building-dreamteamer` → `references/before-you-build.md` |
64
64
 
65
65
  Domain work — meetings, contacts, tasks, content, design — is owned by the **module** that ships those
66
- collections, not by core. Read that module's own skills. Core knows about entity kinds, `users` and
67
- `repos`, and deliberately nothing else.
66
+ collections, not by core. Read that module's own skills. Core knows about entity kinds and `repos`,
67
+ and deliberately nothing else — including nothing about people. There is no `users` collection.
68
68
 
69
69
  ## conventions
70
70
 
@@ -84,9 +84,10 @@ collections, not by core. Read that module's own skills. Core knows about entity
84
84
  - **validate after bulk edits**: `npm run check` reports violations and never modifies files.
85
85
  - workspace-level rules live in `CLAUDE.md`, and a workspace's decision log (where one exists) wins
86
86
  over older documents.
87
- - **session greeting** — surface the operator's inbox: `npm run --silent dt -- tasks list --assignee
88
- users/<user> --status todo`. ⚠ the current user is a record in `data/users/` whose id is
89
- `slug(git config user.name)`; when those disagree the inbox comes back **empty with no error**.
87
+ - **session greeting** — surface the operator's inbox from whatever collection this workspace uses for
88
+ work, e.g. `npm run --silent dt -- tasks list --status todo`. ⚠ **there is no `users` collection and
89
+ no `@me`** (both removed in 0.8.0); read the operator from `git config user.name` at the point you
90
+ need one, and never filter on a person unless this workspace owns a collection of them.
90
91
 
91
92
  ## common mistakes
92
93
 
@@ -95,6 +96,6 @@ collections, not by core. Read that module's own skills. Core knows about entity
95
96
  | editing something under `.dreamteamer/` | generated + gitignored; the change vanishes on the next compile |
96
97
  | changing a source and not compiling | the harness and `check` still read the stale runtime |
97
98
  | hand-writing a record the CLI could add | skips validation, id generation and defaults |
98
- | bare refs (`ada`, `data/users/x.user.md`) | refs are `<collection>/<id>`; anything else fails check |
99
+ | bare refs (`ada`, `data/contacts/x.contact.md`) | refs are `<collection>/<id>`; anything else fails check |
99
100
  | assuming a write was committed | it was not, unless `auto-commit` is on — `dt status` says what is pending |
100
101
  | `git add -A` in a shared tree | steals another session's uncommitted work, invisibly |
@@ -68,7 +68,7 @@ fields, the body is the single `x-body: true` field):
68
68
  ---
69
69
  title: Fix login flow
70
70
  status: todo
71
- assignee: users/ada
71
+ assignee: contacts/ada
72
72
  due: '2026-07-28'
73
73
  ---
74
74
  Users report the login button does nothing on mobile.
package/src/compile.js CHANGED
@@ -520,10 +520,13 @@ export function compile({ root, pkg }) {
520
520
  const base = group.find((g) => !g.doc.extends);
521
521
  if (base) collOwner.set(name, base.moduleName);
522
522
  }
523
- // The engine's own nine collections are an implicit dependency of every module: seven entity
524
- // kinds plus the two the compiler materializes. Requiring every module to declare a dependency
525
- // on the host it cannot run without would be ceremony, not verification.
526
- const CORE_COLLECTIONS = new Set([...KINDS, ...DERIVED_KINDS, 'users', 'repos']);
523
+ // The engine's own nine collections are an implicit dependency of every module: the entity kinds
524
+ // the compiler materializes, plus `repos` (because `repos ensure` clones them). Requiring every
525
+ // module to declare a dependency on the host it cannot run without would be ceremony, not
526
+ // verification. `users` was in this set until 0.8.0 — a module still declaring
527
+ // `x-reference: users` now FAILS here, which is the intended loud outcome rather than a ref
528
+ // pointing at a collection nothing provides.
529
+ const CORE_COLLECTIONS = new Set([...KINDS, ...DERIVED_KINDS, 'repos']);
527
530
  const wsDir = config['workspace-module'];
528
531
  const wsModuleName = wsDir
529
532
  ? sources.find((s) => rel(s.root) === path.join('modules', wsDir))?.name
@@ -802,6 +805,12 @@ export function compile({ root, pkg }) {
802
805
  // fail at compile, not silently at render (review finding 5)
803
806
  const badOps = view?.filter ? [...unknownOperators(view.filter)] : [];
804
807
  if (badOps.length) fail(`${rt}: unknown filter operator(s) ${badOps.join(', ')}`);
808
+ // `@me` died with the `users` collection in 0.8.0. It expanded to `users/<slug>`, so on this
809
+ // engine it can only ever match nothing — and a filter that narrows to zero rows is the exact
810
+ // silent failure this block exists to prevent. Refuse it by name, with the fix.
811
+ if (view?.filter && JSON.stringify(view.filter).includes('"@me"')) {
812
+ fail(`${rt}: filter uses "@me", which was removed with the \`users\` collection in 0.8.0 — it would now match nothing.\n filter on a field this workspace owns instead (e.g. { status: { _eq: "todo" } }).`);
813
+ }
805
814
  }
806
815
 
807
816
  // ---- command-binding validation --------------------------------------------------
package/src/init.js CHANGED
@@ -4,8 +4,6 @@
4
4
  import { execFileSync, spawnSync } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import { dump } from './yaml.js';
8
- import { slugOrHash } from './template.js';
9
7
  import { discoverModules, KINDS } from './compile.js';
10
8
  import { Store } from './store.js';
11
9
 
@@ -76,7 +74,17 @@ export function init({ flags = {} } = {}) {
76
74
  'data-path': dataPath,
77
75
  harnesses,
78
76
  'gitignore-runtime-folder': true,
79
- 'workspace-module': name, // workspace-owned system sources live in modules/<name>/ data and logic stay separated
77
+ // The workspace's own sources live in `modules/default/`, and the folder is named for its ROLE,
78
+ // not for the vault. It used to be named after the workspace, and that name went stale twice in
79
+ // one repo (`hq3` → `gk`, decision 213 reversed by 224) — each rename rewriting every path that
80
+ // RESOLVES while the historical documents deliberately kept the old spelling, so a stale-looking
81
+ // `modules/hq3` was correct in prose and a bug in a path. A role name cannot go stale.
82
+ //
83
+ // `default` is deliberately the same word `RESERVED_NAMESPACES` holds (namespace.js): this module
84
+ // owns the DEFAULT-namespace collections, and the default namespace is the empty prefix. The one
85
+ // misreading it invites — `default/tasks` — is a compile error whose message states the rule.
86
+ // Override with `--workspace-module <name>` if a workspace wants its own spelling.
87
+ 'workspace-module': flags['workspace-module'] ?? 'default',
80
88
  'git-modules': {},
81
89
  disable: [],
82
90
  ...pkg.dreamteamer,
@@ -102,23 +110,16 @@ export function init({ flags = {} } = {}) {
102
110
  fs.mkdirSync(path.join(root, dataPath), { recursive: true });
103
111
  fs.mkdirSync(path.join(root, 'state'), { recursive: true });
104
112
 
105
- // seed the operator's user record from the git identity.
113
+ // NO user record is seeded, and there is no `users` collection — both removed from core in 0.8.0.
114
+ // It failed the "does the ENGINE read it?" test on a circular justification: `users` was core
115
+ // because `@me` resolved against it, and `@me` existed because `users` was core. Nothing else in
116
+ // the compiler, the store or `check` ever read a user record. One record per workspace, whose only
117
+ // job was to restate `git config user.name` in a file that then had to AGREE with it — and when it
118
+ // disagreed the symptom was an empty inbox with no error (decision 99b), a trap that now cannot
119
+ // happen because there is nothing to disagree with.
106
120
  //
107
- // The id MUST be slugOrHash(git user.name) that is exactly how `@me` resolves in filters and
108
- // ui-views (server.js / fsdata.ts), so seeding it from the same source is what makes the core
109
- // /inbox view work by construction. A workspace where a user record is authored by hand under a
110
- // different id gets an EMPTY inbox with no error (decision 99b) — hence the setup-script step in
111
- // any workspace with a second person.
112
- //
113
- // No `everyone` team is seeded: `teams` was removed from core 2026-07-31. It was a one-record
114
- // abstraction with no reader — nothing in the engine, in `check`, or in any view resolved a team.
115
- const gitName = tryGit(root, ['config', 'user.name']) ?? 'operator';
116
- const gitEmail = tryGit(root, ['config', 'user.email']);
117
- const userId = slugOrHash(gitName);
118
- const usersDir = path.join(root, dataPath, 'users');
119
- fs.mkdirSync(usersDir, { recursive: true });
120
- const userFile = path.join(usersDir, `${userId}.user.yaml`);
121
- if (!fs.existsSync(userFile)) fs.writeFileSync(userFile, dump({ name: gitName, ...(gitEmail ? { email: gitEmail } : {}) }));
121
+ // A workspace that needs people as records ships its own collection (a module's `contacts` already
122
+ // does), and reads the operator from git where it needs one. `teams` went the same way 2026-07-31.
122
123
 
123
124
  // .gitignore + .env.example (append-if-missing, never clobber)
124
125
  appendMissing(path.join(root, '.gitignore'), GITIGNORE);
package/src/schema-ops.js CHANGED
@@ -64,6 +64,37 @@ export function workspaceSystemDir(ws, kind) {
64
64
  return kindDir(wm ? path.join(ws.root, 'modules', wm) : ws.root, kind);
65
65
  }
66
66
 
67
+ /**
68
+ * WHERE A COLLECTION'S DESCRIPTOR ACTUALLY LIVES — asked of the manifest, not assumed.
69
+ *
70
+ * `renameCollection` used to derive this from `workspaceSystemDir`, which silently meant "only the
71
+ * workspace module's own collections can be renamed". That is the wrong line. The guard exists to
72
+ * stop a write that will be ERASED, and the thing that erases writes is `npm install` — so the test
73
+ * is `node_modules/`, not "which module". A module whose sources are inline in the workspace repo is
74
+ * under the same git history as everything else and is perfectly safe to rewrite; refusing it made
75
+ * `collections rename` unusable for exactly the migration it was built for, because a workspace's
76
+ * domain collections almost always live in a module.
77
+ *
78
+ * Returns `{ dir, sources }` — the kind dir to write into (the SAME module the descriptor came from,
79
+ * so a rename never teleports a collection into the workspace module), and every descriptor source
80
+ * that contributed, so the caller can refuse the cases this cannot honestly do.
81
+ */
82
+ function descriptorSourceDir(ws, name) {
83
+ const entry = readManifest(ws.root)?.entries?.[`collections/${name}.collection.yaml`];
84
+ // `sources` mixes the descriptor with any collection-templates it merged, so match on the shape
85
+ // of a descriptor path for THIS collection. A namespaced name is nested, hence the full suffix.
86
+ const suffix = `collections/${name}.collection.yaml`;
87
+ const sources = (entry?.sources ?? [])
88
+ .map((s) => s.path)
89
+ .filter((p) => p.endsWith(suffix));
90
+ if (!sources.length) return { dir: null, sources };
91
+ // The BASE descriptor is the one to move. With an overlay present there are two, and the overlay's
92
+ // `extends` names the base by its old qualified id — rewriting that is a second, different
93
+ // migration, so the caller refuses rather than half-doing it.
94
+ const moduleRoot = path.join(ws.root, sources[0].slice(0, sources[0].length - suffix.length));
95
+ return { dir: kindDir(moduleRoot, 'collections'), sources };
96
+ }
97
+
67
98
  // ---- ops ------------------------------------------------------------------------
68
99
 
69
100
  export function createCollection(ws, store, { name, template, namespace }) {
@@ -143,6 +174,15 @@ export function removeCollection(ws, store, name, { force = false } = {}) {
143
174
  * already scopes prose to `[[wikilinks]]` (decision 7) — a fresh `oldName/` pattern would have to
144
175
  * relearn both, and would corrupt `data/tasks/` in a path or a URL on its first outing. N passes over
145
176
  * the record files is the price, and at human scale it is worth paying for reusing the correct code.
177
+ *
178
+ * ⚠ MEASURED 2026-08-17, so the cost is a number rather than a hope: a 2,291-record collection in a
179
+ * 3,391-file workspace — gk-brain's `finance-transactions` — takes **3 minutes**, of which 142s is
180
+ * system time. That is 7.7M file reads to rewrite ZERO references, because the pass runs per id
181
+ * whether or not anything points at the collection. Tolerable for a one-time migration and left
182
+ * alone on that basis; it is O(records x files), so a workspace 3x larger pays 27 minutes. The fix
183
+ * when it is needed is a batch entry point on the store that reads each file ONCE and loops the ref
184
+ * set in memory, with `text.includes(oldName + '/')` as a cheap NEGATIVE filter only — never as the
185
+ * matcher, for the reason above.
146
186
  */
147
187
  export function renameCollection(ws, store, oldName, newName) {
148
188
  const d = store.descriptor(oldName); // throws with the known-collection list if absent
@@ -156,10 +196,21 @@ export function renameCollection(ws, store, oldName, newName) {
156
196
  if (store.descriptors.has(newName)) throw new Error(`collection "${newName}" already exists`);
157
197
  if (d.storage.base === 'runtime') throw new Error(`"${oldName}" is a compiled source, not a data collection — it cannot be renamed`);
158
198
 
159
- const src = path.join(workspaceSystemDir(ws, 'collections'), `${oldName}.collection.yaml`);
160
- const dest = path.join(workspaceSystemDir(ws, 'collections'), `${newName}.collection.yaml`);
199
+ // The descriptor is renamed IN THE MODULE THAT SHIPS IT — see `descriptorSourceDir`. Two cases
200
+ // this refuses, both because doing them halfway is worse than not doing them:
201
+ const { dir: sourceDir, sources } = descriptorSourceDir(ws, oldName);
202
+ if (sources.length > 1) {
203
+ throw new Error(`"${oldName}" is overlaid — ${sources.length} modules contribute a descriptor (${sources.join(', ')}).\n the overlay's \`extends\` names the base by its current id, so renaming the base alone would break it. merge or remove the overlay first.`);
204
+ }
205
+ if (sources.some((p) => p.split(path.sep).includes('node_modules'))) {
206
+ throw new Error(`"${oldName}" ships from node_modules (${sources[0]}) — a write there is erased by the next \`npm install\`. rename it in its own repo and release, or overlay it with \`extends\`.`);
207
+ }
208
+ const src = sourceDir
209
+ ? path.join(sourceDir, `${oldName}.collection.yaml`)
210
+ : path.join(workspaceSystemDir(ws, 'collections'), `${oldName}.collection.yaml`);
211
+ const dest = path.join(sourceDir ?? workspaceSystemDir(ws, 'collections'), `${newName}.collection.yaml`);
161
212
  if (!fs.existsSync(src)) {
162
- throw new Error(`"${oldName}" is not workspace-ownedit ships with a module, so rename it there (or overlay it with \`extends\`)`);
213
+ throw new Error(`"${oldName}" has no writable descriptor source the manifest names none under a module in this workspace. it may be contributed by the engine itself; overlay it with \`extends\` instead.`);
163
214
  }
164
215
 
165
216
  const doc = load(fs.readFileSync(src, 'utf8'));
package/src/server.js CHANGED
@@ -14,12 +14,6 @@ import { matchesFilter } from './filter.js';
14
14
  import { sortRows } from './temporal.js';
15
15
  import { commandsFor, recordResolver } from './record-commands.js';
16
16
  import { distinctValues } from './field-values.js';
17
- import { slugOrHash } from './template.js';
18
-
19
- // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
20
- // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
21
- // reached the user's terminal. stdout stays piped because we read it.
22
- const QUIET = ['ignore', 'pipe', 'ignore'];
23
17
 
24
18
 
25
19
  export function startServer(ws, { port = 8080, host = '127.0.0.1' } = {}) {
@@ -44,13 +38,10 @@ export function startServer(ws, { port = 8080, host = '127.0.0.1' } = {}) {
44
38
  const api = express.Router();
45
39
  api.use((req, res, next) => { freshStore(); next(); });
46
40
 
47
- // current operator id same rule init seeds users with (slugOrHash of git user.name),
48
- // so `@me` filters in ui-views resolve to the seeded user record.
49
- let operatorId = null;
50
- try {
51
- operatorId = slugOrHash(execFileSync('git', ['config', 'user.name'], { cwd: ws.root, stdio: QUIET }).toString().trim());
52
- } catch { /* no git identity — @me filters simply won't narrow */ }
53
-
41
+ // NO `user` on /info since 0.8.0, and no `@me` — both went with the `users` collection. The
42
+ // token expanded to the literal string `users/<slug of git user.name>`, so with no such
43
+ // collection it could only ever produce a dangling reference. A workspace that wants
44
+ // operator-scoped views filters on a field it owns.
54
45
  api.get('/info', (req, res) => {
55
46
  const manifest = readManifest(ws.root);
56
47
  const stale = staleness(ws.root);
@@ -60,7 +51,6 @@ export function startServer(ws, { port = 8080, host = '127.0.0.1' } = {}) {
60
51
  compiled: manifest?.compiled,
61
52
  modules: manifest?.modules ?? [],
62
53
  ui: manifest?.ui ?? [], // module UI bundles staged at /ui/<name>/app.js
63
- user: operatorId,
64
54
  stale: stale.stale?.length ?? 0,
65
55
  collections: [...store.descriptors.keys()],
66
56
  });
@@ -1,21 +0,0 @@
1
- name: users
2
- storage: { path: data/users, codec: yaml, shape: file, suffix: user }
3
- id: { generate: "{{ name | slug }}" }
4
- schema:
5
- type: object
6
- required: [name]
7
- properties:
8
- name:
9
- type: string
10
- description: The person's name. Its slug is the id, and `@me` resolves against it — so it must agree with `git config user.name`, or an assignee query comes back empty with no error.
11
- description:
12
- type: string
13
- description: Who this person is, in one line.
14
- email:
15
- type: string
16
- format: email
17
- description: Their email address.
18
- order: 140
19
- list_fields: [name, last-modified, email]
20
- icon: badge
21
- group: system