dreamteamer 0.27.0 → 0.29.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/README.md CHANGED
@@ -150,6 +150,9 @@ adapters: Claude Code, Codex, Pi, Gemini CLI, Cursor. Author a skill once; every
150
150
 
151
151
  ## The editor
152
152
 
153
+ Extension id `dreamteamer.dreamteamer-vscode` (Marketplace · Open VSX). `init` and `compile` write the
154
+ `.vscode/extensions.json` recommendation, and `dt status` reports whether it is active.
155
+
153
156
  [dreamteamer-vscode](https://github.com/dreamteamer/dreamteamer-vscode) gives you tables, boards,
154
157
  calendars, maps, forms and a data-model designer over the same files — and it loads **the engine your
155
158
  workspace pins**, so the editor, the CLI and any agent session are provably running the same code.
@@ -4,10 +4,24 @@ id: { generate: "{{ name | slug }}" }
4
4
  schema:
5
5
  type: object
6
6
  required: [name, schema]
7
+ # The fields fall into five groups, in this order: IDENTITY (name · singular · title ·
8
+ # title_template — what the collection and a record of it are called), RETRIEVAL (description ·
9
+ # use_when — what brings a session here), SHAPE (extends · schema · id · sensitive), PLACEMENT
10
+ # (storage · module · group — where records live and who owns them), PRESENTATION (order ·
11
+ # list_fields · sort_field · icon · ui — how the surfaces show it). A new field joins one of
12
+ # these; a field that fits none is a sign it belongs on a record, not on the collection.
7
13
  properties:
8
14
  name:
9
15
  type: string
10
16
  description: The collection id — must equal the filename, and must be unique across every installed module.
17
+ singular:
18
+ type: string
19
+ description: >-
20
+ The word the CLI accepts beside `name` — `dt add task …` for `tasks`. DERIVED by inflection
21
+ when absent (`tasks` → `task`, `companies` → `company`, `rnd/projects` → `rnd/project`) and
22
+ authored only where inflection is wrong (`people` → `person`, `meeting-analyses` →
23
+ `meeting-analysis`). Typed input only: a REFERENCE inside a record still spells the full
24
+ name. compile refuses two collections whose name or singular coincide.
11
25
  description:
12
26
  type: string
13
27
  description: >-
@@ -53,6 +67,15 @@ schema:
53
67
  have lived in `modules/<module>/<kind>/` since the 2026-08-05 flatten, and a `system/`
54
68
  prefix is only how `runtime.js` recognises a RUNTIME collection in a descriptor compiled
55
69
  by a pre-flatten engine.
70
+ driver:
71
+ type: string
72
+ description: >-
73
+ The name of an engine DRIVER that answers this collection's verbs instead of a folder of
74
+ files — `docker` is the one shipped (src/containers.js: `containers`, `images`). A driver
75
+ collection has no records on disk: its derived `path` names a folder that never exists,
76
+ so check, commit and the store read zero records and never write one; the CLI and the REST
77
+ route dispatch to the driver first. A string checked against the drivers the engine has,
78
+ not an enum — one implementation, and a second is a code change, not a vocabulary change.
56
79
  codec:
57
80
  type: string
58
81
  enum: [md, yaml, json, file]
@@ -0,0 +1,81 @@
1
+ name: containers
2
+ description: >-
3
+ A workspace running as a container — one person's editor, harness and compiled workspace over a
4
+ named volume set, answered over the Docker Engine API rather than from a folder of files.
5
+ use_when: >-
6
+ a workspace has to RUN somewhere for someone — `dt start container <name> --template <t>` makes
7
+ one and prints its editor URL; `dt list containers` says what is running and where; `stop`, `open`,
8
+ `rm` are the lifecycle. Not a record: nothing lands under data/, nothing is committed, and Docker
9
+ keeps the history
10
+ # MACHINERY WITH A DRIVER, NOT A RECORD COLLECTION. `storage.driver: docker` says every verb on this
11
+ # collection is answered by src/containers.js over the Engine API: `list` is GET /containers/json,
12
+ # `get` is an inspect, `add`/`start` create-and-start, `rm` removes. The derived storage.path names a
13
+ # folder that never exists, so every walker (check, commit, the store's index) reads zero records here
14
+ # and never writes one; the REST route and the CLI dispatch to the driver before the store is asked.
15
+ # `group: system` folds it out of the orientation block's domain listing, beside `repos` and the
16
+ # compiled kinds, which is where a stranger's mental model puts "the thing my workspace runs in".
17
+ storage:
18
+ driver: docker
19
+ schema:
20
+ type: object
21
+ required: [name, template]
22
+ properties:
23
+ # ---- identity ----
24
+ name:
25
+ type: string
26
+ description: The workspace name — also the container name and the stem of its three volumes (`dreamteamer-<name>-workspace` · `-home` · `-files`).
27
+ template:
28
+ type: string
29
+ description: The template it was made from — an image carrying `dreamteamer.template`, `dreamteamer.ports` and `dreamteamer.modules` labels; `--template hq` resolves to `<DT_REGISTRY>/hq:<DT_TEMPLATE_TAG>` or to `DT_IMAGE_hq`.
30
+ image:
31
+ type: string
32
+ description: The image reference the container runs.
33
+ id:
34
+ type: string
35
+ description: Docker's short id.
36
+ # ---- runtime ----
37
+ state:
38
+ type: string
39
+ enum: [created, running, paused, restarting, exited, dead]
40
+ description: Docker's container state.
41
+ status:
42
+ type: string
43
+ description: Docker's own phrase — `Up 2 hours`, `Exited (0) 3 minutes ago`.
44
+ started:
45
+ type: string
46
+ format: date-time
47
+ description: When it last started.
48
+ # ---- network ----
49
+ editor_url:
50
+ type: string
51
+ description: Where the person opens the editor — `http://localhost:<port>/?folder=<workspace dir>`; the host port comes from DT_PORT_BASE upward, bound to DT_BIND (127.0.0.1).
52
+ port:
53
+ type: integer
54
+ description: The host port the editor is published on.
55
+ bind:
56
+ type: string
57
+ description: The host address it is bound to — loopback unless the host .env says otherwise.
58
+ # ---- storage ----
59
+ workspace_dir:
60
+ type: string
61
+ description: Where the workspace is mounted inside — `/workspaces/<name>`, the dev-container convention.
62
+ volumes:
63
+ type: object
64
+ description: The three named volumes — `workspace` (the repo), `home` (the person's logins and editor settings), `files` (FILES_FOLDER). Plain `rm` keeps them; `rm --force` removes them.
65
+ mounts:
66
+ type: array
67
+ items: { type: string }
68
+ description: Extra bind or volume mounts passed as `--mount <host-path|volume>:<container-path>[:ro]`.
69
+ # ---- person ----
70
+ person:
71
+ type: string
72
+ description: Whose container this is — the git identity injected as env; the harness login happens INSIDE, once, and is never injected.
73
+ # ---- provenance ----
74
+ created:
75
+ type: string
76
+ format: date-time
77
+ description: When Docker created it.
78
+ order: 146
79
+ list_fields: [name, template, state, editor_url, person]
80
+ icon: deployed_code
81
+ group: system
@@ -0,0 +1,46 @@
1
+ name: images
2
+ description: >-
3
+ A template a workspace container is made from — a Docker image carrying the `dreamteamer.template`,
4
+ `dreamteamer.ports` and `dreamteamer.modules` labels — as the local Docker holds it.
5
+ use_when: >-
6
+ choosing what a container runs — `dt list images` shows the templates present, `dt add image
7
+ --template <t>` pulls one, `dt rm image <ref>` removes one; a template is an IMAGE WITH LABELS, not
8
+ a record here, so there is nothing to author — publish an image with the labels and it appears
9
+ # The second driver-backed collection (see containers.collection.yaml for what that means). `list` is
10
+ # GET /images/json filtered to the template label, `get` an inspect, `add --template` a pull, `rm` a
11
+ # delete. No records, no folder, no commit.
12
+ storage:
13
+ driver: docker
14
+ schema:
15
+ type: object
16
+ required: [image]
17
+ properties:
18
+ # ---- identity ----
19
+ image:
20
+ type: string
21
+ description: The image reference — `<registry>/<template>:<tag>`.
22
+ template:
23
+ type: string
24
+ description: The `dreamteamer.template` label — the word `--template` takes.
25
+ id:
26
+ type: string
27
+ description: Docker's image id.
28
+ # ---- what it runs ----
29
+ ports:
30
+ type: string
31
+ description: The `dreamteamer.ports` label — the in-container port the editor listens on (8080 by convention).
32
+ modules:
33
+ type: string
34
+ description: The `dreamteamer.modules` label — the modules the template installs, comma-separated.
35
+ # ---- size and time ----
36
+ size_mb:
37
+ type: integer
38
+ description: Image size in megabytes.
39
+ created:
40
+ type: string
41
+ format: date
42
+ description: When the image was built.
43
+ order: 147
44
+ list_fields: [template, image, size_mb, created]
45
+ icon: layers
46
+ group: system
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
4
4
  "description": "A workspace compiler for coding agents — 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>",
@@ -29,7 +29,8 @@
29
29
  "node": ">=20"
30
30
  },
31
31
  "bin": {
32
- "dreamteamer": "bin/dreamteamer.js"
32
+ "dreamteamer": "bin/dreamteamer.js",
33
+ "dt": "bin/dreamteamer.js"
33
34
  },
34
35
  "files": [
35
36
  "NOTICE",
@@ -49,7 +50,7 @@
49
50
  "express": "^5.2.1",
50
51
  "fractional-indexing": "^4.0.0",
51
52
  "js-yaml": "^4.1.0",
52
- "yaml": "2.8.1"
53
+ "yaml": "2.8.4"
53
54
  },
54
55
  "dreamteamer": {
55
56
  "title": "System"
@@ -59,6 +59,7 @@ their flags, on one page (there is no per-verb `--help`).
59
59
  the verb names, as a map (semantics and flags live in `help`; a test holds this list to the
60
60
  dispatch, so it cannot drift):
61
61
 
62
+ - a collection may be spelled in the SINGULAR on any of these (`dt add task "call the bank"` — one bare positional is the title); references inside records still spell the full name
62
63
  - read & measure — `list` `get` `values` `history` `diff` `next` `relations` `resolve`
63
64
  - write & publish — `add` `set` `rm` `rename` `move` `revert` `commit`
64
65
  - fields (sources, through the compile gate) — `add-field` `set-field` `rm-field` `rename-field` (system entities — modules, collections, skills, ui-views… — take the RECORD verbs above)
@@ -112,3 +112,24 @@ prefers. Nothing about the code moves; `check` and `compile` read only what desc
112
112
  | hand-writing the first descriptor | `dt add collections` is compile-gated and publishes itself; hand-written sources owe `dt compile` |
113
113
  | rewriting existing files to fit a guessed schema | describe reality, compile, `check` — then decide which violations are worth fixing in the data |
114
114
  | waiting for a UI before starting | the CLI and the records are the complete system; any surface renders them later, unchanged |
115
+
116
+ ## The editor
117
+
118
+ The VS Code-family extension is **`dreamteamer.dreamteamer-vscode`** (Marketplace and Open VSX). It
119
+ loads the engine the workspace pins, so the editor, the CLI and an agent session run the same code.
120
+ `init` writes `.vscode/extensions.json` recommending it and `compile` keeps that file current, so a
121
+ VS Code, Cursor or code-server window opened on the workspace offers to install it — that prompt is
122
+ the intended path. From a terminal:
123
+
124
+ ```bash
125
+ code --install-extension dreamteamer.dreamteamer-vscode # VS Code (on some machines `code` is Cursor)
126
+ code-server --install-extension dreamteamer.dreamteamer-vscode \
127
+ --extensions-dir <the dir the running server was started with> # match its --extensions-dir, or the window never sees it
128
+ ```
129
+
130
+ ⚠ Inside code-server's own terminal (or an agent it started) that command fails with `error not spawned
131
+ with IPC`: the shell inherits code-server's `VSCODE_*` / `CODE_SERVER_PARENT_PID` variables and the
132
+ CLI thinks it is a forked child. Prefix it with `env -u VSCODE_ESM_ENTRYPOINT -u VSCODE_HANDLES_SIGPIPE
133
+ -u VSCODE_HANDLES_UNCAUGHT_ERRORS -u VSCODE_NLS_CONFIG -u VSCODE_CWD -u VSCODE_RECONNECTION_GRACE_TIME
134
+ -u CODE_SERVER_PARENT_PID`, or let the recommendation prompt do the install. Whether the extension is
135
+ ACTIVE is `dt status`'s `editor:` line — it reads the marker the extension writes on activation.
package/src/cli.js CHANGED
@@ -16,6 +16,7 @@ import { findWorkspace } from './workspace.js';
16
16
  import { compile, staleness, warnIfStale, discoverModules, CHANNEL_LABEL, locationOf, KINDS } from './compile.js';
17
17
  import { check } from './check.js';
18
18
  import { collectionCommand, emit, relationsCommand, parseArgs, refuseUnknownFlags } from './collections-cli.js';
19
+ import { driverTarget, driverCommand, setup as hostSetup, parseFlags as hostFlags, DRIVER_VERBS, LIFECYCLE_VERBS, CONTAINER_FLAGS } from './containers.js';
19
20
  import { init, installClone, update, listRepos } from './init.js';
20
21
  import { installCommand, describeCheckout, listWorktrees, worktreeCommand } from './checkout.js';
21
22
  import { proveCommand, readLedger, flagEnabled } from './prove.js';
@@ -23,7 +24,7 @@ import { landCommand } from './land.js';
23
24
  import { deriveEvents } from './events.js';
24
25
  import { commitPending } from './commit.js';
25
26
  import { Store } from './store.js';
26
- import { splitRef } from './ref.js';
27
+ import { splitRef, canonicalCollection } from './ref.js';
27
28
  import { envContext, renderTemplate } from './env-vars.js';
28
29
  import { exportCommand, EXPORT_FLAGS } from './export-notebooklm.js';
29
30
 
@@ -57,8 +58,12 @@ the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee i
57
58
  case-insensitive variants; date-times sort and
58
59
  compare as instants, across offsets)
59
60
  get <collection>/<id> [--json]
60
- add <collection> --<field> <value> … [--id <explicit-id>]
61
- (a codec-file collection takes --from <path>
61
+ add <collection> ["<title>"] --<field> <value> … [--id <explicit-id>]
62
+ (ONE bare positional fills the collection's title
63
+ field — the one its title_template names — so
64
+ dt add task "call the bank" is
65
+ dt add tasks --name "call the bank".
66
+ a codec-file collection takes --from <path>
62
67
  instead — the file IS the record, fields derive;
63
68
  --force replaces an existing file record.
64
69
  A repeated --<field> is one ELEMENT of an array
@@ -164,6 +169,12 @@ collection: the ENGINE does not read one, and \`rename-field\` was the only capa
164
169
  title_template, id.generate, a ui-view's options.columns and filter,
165
170
  and a command-binding's can-enter/can-exit. ONE commit)
166
171
 
172
+ Every <collection> above may be spelled in the SINGULAR: dt add task …, dt get task/<id>,
173
+ dt list meeting-analysis, dt add-field task …. The singular is derived from the descriptor
174
+ (tasks → task, companies → company) and authored on it as singular: where inflection is
175
+ wrong (people → person); compile refuses two collections whose words collide. A REFERENCE
176
+ inside a record still names the collection in full — tasks/kickoff, never task/kickoff.
177
+
167
178
  Every verb that MOVES records or CLEARS values takes --dry-run and prints its plan first:
168
179
  records N · refs M · descriptors K · values cleared V
169
180
 
@@ -232,6 +243,31 @@ workspace verbs:
232
243
  and one \`proofs:\` line counting each proof's LAST verdict on this machine
233
244
  [--strict] exit 1 when any proof's ledger tail is a FAIL
234
245
  start serve the clean REST api at /api [--port <n>]
246
+
247
+ containers — a workspace as a running container (Docker Engine API over its socket, no dependency;
248
+ these verbs work with NO workspace, so npm i -g dreamteamer and Docker Desktop are enough):
249
+ setup make THIS MACHINE ready: checks Docker, writes ~/.dreamteamer/.env with its defaults
250
+ (DT_PORT_BASE 8100 · DT_BIND 127.0.0.1 · DT_REGISTRY · DT_TEMPLATE_TAG ·
251
+ DT_DOCKER_TIMEOUT 30 — seconds a request to Docker may sit idle before the verb
252
+ fails), lists the templates present, pulls one on request [--template <t>] [--json]
253
+ start container <name> --template <t> create-if-absent and start: a code-server editor at
254
+ http://localhost:<port>/?folder=/workspace over a compiled workspace, three named volumes
255
+ (workspace · home · files), image <DT_REGISTRY>/<template>:<tag> or DT_IMAGE_<template>.
256
+ The workspace is mounted at /workspaces/<name>. Idempotent. NO token is ever injected —
257
+ log in INSIDE, once; the home volume keeps it.
258
+ [--repo <git url>] clone an EXISTING workspace into the volume on first start, instead
259
+ of laying the template down — how a person joins one on GitHub
260
+ [--mount <host-path|volume>:<container-path>[:ro]] extra mounts, repeatable
261
+ [--name <git name>] [--email <git email>] [--no-open] [--json]
262
+ stop container <name> stop it; every volume kept [--json]
263
+ open container <name> print (and open) its editor URL [--no-open]
264
+ [--vscode] print (and open) the Dev Containers attach URI instead — the host's own
265
+ VS Code inside the container, extensions from the image's metadata label
266
+ list containers | images the record verbs, answered over Docker instead of a
267
+ get container <name> | image <ref> folder — singular or plural, either spelling.
268
+ rm container <name> [--force] plain rm keeps the volumes; --force removes them too
269
+ add image --template <t> pull a template's image; rm image <ref> removes one
270
+
235
271
  changes what changed in every repo that holds records, as record events
236
272
  [--since <sha|YYYY-MM-DD>] (default: HEAD~1 — the last commit's own changes) [--json]
237
273
  commit publish records already written to disk: samples git status over every
@@ -286,7 +322,10 @@ export const GLOBAL_FLAGS = ['vault'];
286
322
  export const WORKSPACE_FLAGS = {
287
323
  init: ['name', 'data-path', 'harnesses', 'workspace-module'], update: [],
288
324
  install: ['clone', 'dry-run', 'json', 'link-env', 'all', 'hook', 'print-adapters'],
289
- start: ['port'], compile: ['watch'], check: [], status: ['strict'],
325
+ // `start` is TWO forms: bare, the REST api (--port); with a `container <name>` target, the
326
+ // lifecycle verb — whose flags are the driver's. One table, because `flags-honoured` reads it.
327
+ start: ['port', ...CONTAINER_FLAGS], compile: ['watch'], check: [], status: ['strict'],
328
+ setup: ['template', 'json'], stop: ['json'], open: ['json', 'no-open', 'vscode'],
290
329
  changes: ['since', 'json'], commit: ['dry-run', 'json'],
291
330
  export: EXPORT_FLAGS,
292
331
  // the UNION of every form's flags — the outer typo gate. Which flags each FORM takes is refused
@@ -299,6 +338,15 @@ export const WORKSPACE_FLAGS = {
299
338
  export function run(argv) {
300
339
  const [cmd, ...rest] = argv;
301
340
  try {
341
+ // HOST VERBS resolve BEFORE workspace discovery: `setup`, and any verb whose target is a
342
+ // driver collection (`containers`, `images`, singular or plural). They answer identically on a
343
+ // bare machine — `npm i -g dreamteamer` and Docker Desktop, nothing else — and inside a
344
+ // workspace, because the thing they make IS the workspace (src/containers.js).
345
+ const host = hostDispatch(cmd, rest);
346
+ if (host) {
347
+ host.then((code) => process.exit(code)).catch((e) => { console.error(`✖ ${e.message}`); process.exit(1); });
348
+ return;
349
+ }
302
350
  if (cmd in WORKSPACE_FLAGS) {
303
351
  const bad = rest.filter((a) => a.startsWith('--')).map((a) => a.slice(2).split('=')[0]).find((f) => !WORKSPACE_FLAGS[cmd].includes(f));
304
352
  if (bad) throw new Error(`unknown flag "--${bad}" on \`dt ${cmd}\`\n known: ${WORKSPACE_FLAGS[cmd].map((f) => `--${f}`).join(', ') || '(none — this verb takes no flags)'}`);
@@ -315,7 +363,9 @@ export function run(argv) {
315
363
  for (let i = 0; i < rest.length; i++) if (rest[i].startsWith('--')) flags[rest[i].slice(2)] = rest[i + 1];
316
364
  process.exit(init({ flags }));
317
365
  }
318
- if (!cmd) {
366
+ if (!cmd || cmd === 'help') {
367
+ // `help` works OUTSIDE a workspace too — the host verbs above do, and a person who just ran
368
+ // `npm i -g dreamteamer` on a bare machine has nothing else to read.
319
369
  emit(USAGE);
320
370
  process.exit(0);
321
371
  }
@@ -507,6 +557,16 @@ export function run(argv) {
507
557
  process.exit(1);
508
558
  }
509
559
  console.log(`compiled: ${s.manifest.compiled}`);
560
+ // THE EDITOR, from the marker the extension writes on activation (`.dreamteamer/editor.json`).
561
+ // Without it an agent working inside the editor could not tell whether the extension was
562
+ // installed, active, or refusing the engine — the operator had to report "no icon".
563
+ try {
564
+ const marker = path.join(ws.root, '.dreamteamer', 'editor.json');
565
+ if (fs.existsSync(marker)) {
566
+ const e = JSON.parse(fs.readFileSync(marker, 'utf8'));
567
+ console.log(`editor: ${e.extension ?? 'dreamteamer-vscode'} ${e.version ?? '?'} · ${e.state ?? 'active'} ${e.activated ?? ''} · engine ${e.engine ?? '?'}${e.host ? ` · ${e.host}` : ''}`);
568
+ } else console.log('editor: not detected — the extension dreamteamer.dreamteamer-vscode writes .dreamteamer/editor.json when it activates on this workspace');
569
+ } catch { console.log('editor: marker unreadable — .dreamteamer/editor.json is not JSON'); }
510
570
  // provenance is LIVE discovery (not the manifest) — shows what the next compile would use
511
571
  const { modules, shadows } = discoverModules(ws.root, ws.pkg);
512
572
  const shadowed = new Map(shadows.map((sh) => [sh.name, sh]));
@@ -661,7 +721,8 @@ export function run(argv) {
661
721
  if (!target || target.startsWith('--')) {
662
722
  throw new Error(`dt ${cmd} needs a collection: dreamteamer ${cmd} <collection> --name <field> …`);
663
723
  }
664
- process.exit(collectionCommand(ws, target, cmd, flagArgs));
724
+ // the singular is legal here too: `dt add-field task --name due …`
725
+ process.exit(collectionCommand(ws, canonicalCollection(new Store(ws).descriptors, target) ?? target, cmd, flagArgs));
665
726
  }
666
727
  case 'relations':
667
728
  warnIfStale(ws.root);
@@ -728,6 +789,25 @@ export function run(argv) {
728
789
  }
729
790
  }
730
791
 
792
+ /** The verbs that run with no workspace. Returns a promise of an exit code, or null when the
793
+ * command is not ours and the ordinary workspace dispatch should take it. */
794
+ function hostDispatch(cmd, rest) {
795
+ if (cmd === 'setup') {
796
+ const bad = rest.filter((a) => a.startsWith('--')).map((a) => a.slice(2).split('=')[0]).find((f) => !WORKSPACE_FLAGS.setup.includes(f));
797
+ if (bad) throw new Error(`unknown flag "--${bad}" on \`dt setup\`\n known: ${WORKSPACE_FLAGS.setup.map((f) => `--${f}`).join(', ')}`);
798
+ return hostSetup(hostFlags(rest).flags);
799
+ }
800
+ const target = driverTarget(rest[0]);
801
+ if (target && DRIVER_VERBS.has(cmd)) return driverCommand(cmd, target, rest.slice(1));
802
+ // A lifecycle verb aimed at anything else is refused by name: `dt start tasks` is not a
803
+ // server and not a container, and "unknown collection" would send the reader the wrong way.
804
+ if (LIFECYCLE_VERBS.has(cmd) && rest[0] && !rest[0].startsWith('--')) {
805
+ return Promise.reject(new Error(`\`${cmd}\` is a container lifecycle verb — "${rest[0]}" is not a container. dt ${cmd} container <name>${cmd === 'start' ? ' --template <t>' : ''}${cmd === 'start' ? '; a bare `dt start` serves the REST api' : ''}`));
806
+ }
807
+ if (cmd === 'stop' || cmd === 'open') return Promise.reject(new Error(`dt ${cmd} container <name> — see \`dreamteamer help\``));
808
+ return null;
809
+ }
810
+
731
811
  /** Translate `dt <verb> <target> …` into the noun-verb call the implementation layer takes. */
732
812
  function dispatchRecordVerb(ws, verb, args) {
733
813
  const [target, ...rest] = args;
@@ -735,18 +815,22 @@ function dispatchRecordVerb(ws, verb, args) {
735
815
  // A flag in the target slot is a word-order mistake, not a collection: without this,
736
816
  // `dt list --json contacts` reported `unknown collection "--json"` and dumped every name.
737
817
  if (target.startsWith('--')) throw new Error(`dt ${verb} takes its target BEFORE the flags: dreamteamer ${verb} <target> ${target} …`);
738
- if (COLLECTION_VERBS.has(verb)) return collectionCommand(ws, target, verb, rest);
818
+ // A collection may be named by its declared name OR its singular (`dt add task …`); the
819
+ // canonical name is what every layer below sees. An unknown word passes through unchanged so
820
+ // the store's own "unknown collection" sentence, which lists what exists, is the one printed.
821
+ const { descriptors } = new Store(ws);
822
+ const canonical = canonicalCollection(descriptors, target) ?? target;
823
+ if (COLLECTION_VERBS.has(verb)) return collectionCommand(ws, canonical, verb, rest);
739
824
  if (REF_VERBS.has(verb)) {
740
- const { collection, id } = splitRef(new Store(ws).descriptors, target);
825
+ const { collection, id } = splitRef(descriptors, target);
741
826
  return collectionCommand(ws, collection, verb, [id, ...rest]);
742
827
  }
743
828
  // EITHER_VERBS from here: a bare collection is legal for both — `move <collection> --init`,
744
829
  // `next <collection>`.
745
- const { descriptors } = new Store(ws);
746
- if (descriptors.has(target)) {
830
+ if (descriptors.has(canonical)) {
747
831
  return verb === 'move'
748
- ? collectionCommand(ws, target, 'move', rest)
749
- : collectionCommand(ws, 'commands', 'for', [target, ...rest]);
832
+ ? collectionCommand(ws, canonical, 'move', rest)
833
+ : collectionCommand(ws, 'commands', 'for', [canonical, ...rest]);
750
834
  }
751
835
  const { collection, id } = splitRef(descriptors, target);
752
836
  if (verb === 'move') return collectionCommand(ws, collection, 'move', [id, ...rest]);
@@ -179,6 +179,16 @@ export function collectionCommand(ws, collection, verb, args) {
179
179
  return 0;
180
180
  }
181
181
  if (flags.from) throw new Error(`--from imports a file as a record, and "${collection}" is not a \`codec: file\` collection`);
182
+ // ONE bare positional is the record's title — the field `title_template` names — so
183
+ // `dt add task "call the bank"` reads as a sentence. Two positionals is a mistake (a flag
184
+ // value that lost its flag), and so is giving the title twice; both are refused by name.
185
+ if (pos.length > 1) throw new Error(`dt add ${collection} takes ONE positional (the title) and flags for the rest — got ${pos.length}: ${pos.map((p) => `"${p}"`).join(' ')}`);
186
+ if (pos.length === 1) {
187
+ const titleField = /\{\{\s*([A-Za-z_][\w]*)/.exec(d.title_template ?? '')?.[1];
188
+ if (!titleField || titleField === 'id') throw new Error(`"${collection}" labels its records by id, so there is no title field for "${pos[0]}" to fill — pass fields as --<field> <value>`);
189
+ if (titleField in flags) throw new Error(`the title was given twice — "${pos[0]}" and --${titleField} ${JSON.stringify(flags[titleField])}`);
190
+ flags[titleField] = pos[0];
191
+ }
182
192
  const fields = coerceArrays(d, stripMeta(flags));
183
193
  const { id, file, idFallback } = store.add(collection, fields, { id: flags.id });
184
194
  flags.json
package/src/commit.js CHANGED
@@ -7,7 +7,7 @@ import path from 'node:path';
7
7
  import { pathToRecord } from './events.js';
8
8
  import { parseRecordText } from './records.js';
9
9
  import { relationsOf } from './relations.js';
10
- import { splitRef } from './ref.js';
10
+ import { splitRef, canonicalCollection } from './ref.js';
11
11
 
12
12
  // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
13
13
  // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
@@ -42,7 +42,8 @@ function parseTargets(descriptors, only) {
42
42
  const whole = new Set();
43
43
  const records = new Map();
44
44
  for (const target of only) {
45
- if (descriptors.has(target)) { whole.add(target); scope.add(target); continue; }
45
+ const asCollection = canonicalCollection(descriptors, target);
46
+ if (asCollection) { whole.add(asCollection); scope.add(asCollection); continue; }
46
47
  const { collection, id } = splitRef(descriptors, target);
47
48
  records.set(`${collection}/${id}`, { collection, id });
48
49
  scope.add(collection);