dreamteamer 0.30.0 → 0.32.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.
Files changed (43) hide show
  1. package/README.md +47 -153
  2. package/collections/collections.collection.yaml +26 -9
  3. package/package.json +14 -2
  4. package/skills/using-dreamteamer/SKILL.md +16 -10
  5. package/skills/using-dreamteamer/references/before-you-build.md +4 -3
  6. package/skills/using-dreamteamer/references/collections.md +71 -1
  7. package/skills/using-dreamteamer/references/commands.md +3 -3
  8. package/skills/using-dreamteamer/references/data-modeling.md +9 -1
  9. package/skills/using-dreamteamer/references/extensions.md +60 -0
  10. package/skills/using-dreamteamer/references/records.md +17 -0
  11. package/skills/using-dreamteamer/references/sessions.md +4 -5
  12. package/skills/using-dreamteamer/references/skills.md +3 -5
  13. package/src/api.d.ts +109 -0
  14. package/src/api.js +70 -0
  15. package/src/check.js +57 -2
  16. package/src/checkout.js +105 -330
  17. package/src/cli.js +90 -274
  18. package/src/collections-cli.js +16 -165
  19. package/src/commit.js +7 -0
  20. package/src/compile.js +211 -155
  21. package/src/events.js +7 -2
  22. package/src/extensions.js +135 -0
  23. package/src/filter.js +1 -1
  24. package/src/harnesses.js +82 -135
  25. package/src/init.js +9 -3
  26. package/src/placement.js +209 -0
  27. package/src/records-api.d.ts +103 -0
  28. package/src/records-api.js +40 -0
  29. package/src/runtime.js +2 -2
  30. package/src/schema-ops.js +26 -9
  31. package/src/store.js +396 -35
  32. package/collections/containers.collection.yaml +0 -81
  33. package/collections/images.collection.yaml +0 -46
  34. package/collections/proofs.collection.yaml +0 -96
  35. package/skills/using-dreamteamer/references/exporting.md +0 -53
  36. package/skills/using-dreamteamer/references/proofs.md +0 -435
  37. package/skills/using-dreamteamer/references/worktrees.md +0 -235
  38. package/src/container-archive.js +0 -356
  39. package/src/containers.js +0 -635
  40. package/src/export-notebooklm.js +0 -502
  41. package/src/land.js +0 -743
  42. package/src/prove.js +0 -1922
  43. package/src/server.js +0 -481
package/src/cli.js CHANGED
@@ -12,22 +12,17 @@
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import { execFileSync } from 'node:child_process';
15
- import { findWorkspace } from './workspace.js';
16
- import { compile, staleness, warnIfStale, discoverModules, CHANNEL_LABEL, locationOf, KINDS } from './compile.js';
15
+ import { openWorkspace } from './api.js';
16
+ import { compile, staleness, warnIfStale, discoverModules, CHANNEL_LABEL, locationOf, kindsOf } from './compile.js';
17
17
  import { check } from './check.js';
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';
20
- import { archiveCommand } from './container-archive.js';
18
+ import { collectionCommand, emit, relationsCommand } from './collections-cli.js';
21
19
  import { init, installClone, update, listRepos } from './init.js';
22
- import { installCommand, describeCheckout, listWorktrees, worktreeCommand } from './checkout.js';
23
- import { proveCommand, readLedger, flagEnabled } from './prove.js';
24
- import { landCommand } from './land.js';
20
+ import { installCommand, describeCheckout } from './checkout.js';
25
21
  import { deriveEvents } from './events.js';
26
22
  import { commitPending } from './commit.js';
27
23
  import { Store } from './store.js';
28
24
  import { splitRef, canonicalCollection } from './ref.js';
29
25
  import { envContext, renderTemplate } from './env-vars.js';
30
- import { exportCommand, EXPORT_FLAGS } from './export-notebooklm.js';
31
26
 
32
27
  // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
33
28
  // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
@@ -45,10 +40,6 @@ record verbs (hard validation — invalid writes are rejected before disk).
45
40
  A <target> is either a collection name or a <collection>/<id> reference; the reference splits at
46
41
  the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee is ONE argument:
47
42
  list <collection> [--filter k=v] [--where <json>] [--sort [-]<field>] [--json]
48
- list proofs [--missing] (a proof listing appends two COMPUTED columns:
49
- availability on THIS machine, and the last verdict
50
- from its ledger. --missing inverts it — one line per
51
- artifact no proof is about, so it takes no filter)
52
43
  (--filter is ONE condition — repeat it to AND more
53
44
  (--filter a=1 --filter b=2 wants both);
54
45
  anything compound goes in one --where, operator
@@ -88,6 +79,17 @@ the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee i
88
79
  relations [<collection>] (every two-way pair: owner.field → target.mirror)
89
80
  relations rebuild <collection> [--drop <f>] (regenerate mirror VALUES from the owning side;
90
81
  --drop removes a stale ex-mirror key from records)
82
+ relocate <target> [--dry-run] [--json] (move record FILES to where the compiled descriptor
83
+ puts them — a record stored under another
84
+ collection (storage.under) whose folder disagrees
85
+ with its owner field, or a file record in a
86
+ collection that became shape: folder. Ids and
87
+ references never change; a pending edit on a
88
+ moved file, or a dangling owner, refuses the whole
89
+ plan — commit, or fix the field, first)
90
+ relocate <collection> --to-root [--dry-run] (the REVERSE: every placed record back into the
91
+ collection's own folder, ids unchanged — the step
92
+ before removing or changing storage.under)
91
93
  resolve '<string>' | <collection>/<id> <field>
92
94
  (render \${env:NAME} · \${workspaceFolder} ·
93
95
  \${userHome} — the ONLY substitution point; a
@@ -95,7 +97,8 @@ the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee i
95
97
  field prints one item per line)
96
98
 
97
99
  system verbs — the SAME verbs, on the entities the compiler materializes (modules, collections,
98
- skills, agents, commands, command-bindings, ui-views, collection-templates, proofs). ⚠ ONE
100
+ skills, agents, commands, command-bindings, ui-views, collection-templates, and any kind an
101
+ installed extension adds). ⚠ ONE
99
102
  difference in POLICY, not in spelling: a SYSTEM write commits itself, because an uncompilable or
100
103
  unpublished schema is not a state a workspace should sit in; a RECORD write does not — \`commit\`
101
104
  publishes it. The commit lands in the repo that holds the source, so a write into a git module
@@ -181,7 +184,7 @@ Every verb that MOVES records or CLEARS values takes --dry-run and prints its pl
181
184
 
182
185
  workspace verbs:
183
186
  init write the workspace skeleton into the current directory (never compiles)
184
- [--harnesses claude-code,codex,pi,gemini-cli,cursor,notebooklm]
187
+ [--harnesses claude-code,codex,pi,gemini-cli,cursor]
185
188
  --version print the engine version (works anywhere)
186
189
  install make THIS checkout ready — the engine, .env (linked from the primary when this is a
187
190
  worktree), declared local assets, git modules, compile, and a declared postinstall.
@@ -190,105 +193,20 @@ workspace verbs:
190
193
  [--link-env] link .env even into a worktree OUTSIDE the primary root
191
194
  install --hook | --print-adapters
192
195
  --hook: read a harness hook's JSON payload from stdin and install the checkout its
193
- \`cwd\` names — the process cwd is the harness's, never the worktree's. In a linked
194
- worktree the board's last line is the landing instruction.
195
- --print-adapters: print the hook snippet for each declared harness. Merging it into
196
- the harness's own settings file is the operator's act — the engine never writes one
196
+ \`cwd\` names — the process cwd is the harness's, never the worktree's
197
+ --print-adapters: print the hook snippet for each declared harness, with every hook an
198
+ installed extension adds. Merging it into the harness's own settings file is the
199
+ operator's act — the engine never writes one
197
200
  install repos/<id> | repos --all [--json]
198
201
  materialize an attached repo's working tree ON DEMAND — never as part of making a
199
202
  checkout ready; --all is the explicit opt-in, e.g. before going offline
200
203
  install --clone <url> [name] attach a git module to this workspace
201
- add worktrees --name <n> [--path <dir>] [--base <ref>] [--temp] | --hook
202
- cut a linked git worktree on branch worktree-<n> and \`install\` it, so it is ready
203
- to work in; it prints its absolute path LAST, which is what a creation hook echoes.
204
- --temp: detached, no branch, under .worktrees/.tmp-* inside this root — a sandbox
205
- --hook: take the name from a WorktreeCreate payload on stdin; implies .worktrees/<n>
206
- list worktrees | get worktrees/<n> | rm worktrees/<n> [--force]
207
- observed from \`git worktree list\`, never stored. rm refuses a worktree holding
208
- dirty records or commits not on the primary branch — neither is visible from here
209
- land worktrees/<name|path> [--keep] [--dry-run] [--branch <n>] [--json]
210
- land a worktree's commits onto the primary branch — the one MOVEMENT verb. It refuses
211
- while the primary holds pending record writes, rebases a COPY of the branch under one
212
- engine-owned lock, resolves ONLY the generated harness block, fast-forwards, recompiles
213
- the primary, then removes the worktree and its branch. A conflict in a record aborts
214
- and leaves every tree exactly as it was
215
- [--keep] keep the worktree, reset onto what landed
216
- [--branch <n>] give a DETACHED worktree the branch worktree-<n> first
217
- [--dry-run] print the plan and change nothing
218
- land --hook [--dry-run] [--json]
219
- read a WorktreeRemove payload on stdin and report what that worktree still holds.
220
- ALWAYS a dry run — the engine never lands while a harness is deleting the tree
221
204
  update pull git_modules clones forward (ff-only on the lockfile ref), rebuild,
222
205
  then compile; [<name>] updates just one. dirty clones are skipped
223
206
  compile materialize modules + workspace sources into .dreamteamer (+ harness adapters)
224
207
  [--watch] recompile on source changes
225
208
  check validate every record against the compiled descriptors (report-only)
226
- prove <proof> | <artifact-ref> | --all
227
- run a proof and answer with an EXIT CODE: 0 pass · 1 fail · 3 unavailable (this
228
- machine lacks a required var or binary) · 4 no-fixture (the given matched no record)
229
- · 5 a \`perform\` step is owed a human/agent · 6 vacuous (every expectation already
230
- held, so the proof cannot fail). Evidence lands in .dreamteamer/.proofs/<id>.jsonl
231
- <proof> [--record <c>/<id>] finish the pending run for that record
232
- [--restart] discard a pending run and start over
233
- [--here] run a \`writes\` proof in this workspace, not a sandbox
234
- [--keep] keep the sandbox afterwards — under --json that is
235
- \`kept: true\` beside the \`sandbox\` path [--json] one object on stdout
236
- <artifact-ref> every proof whose \`about\` names skills/<id>, commands/<id>,
237
- command-bindings/<id> or <module>/bin/<file> — board semantics, so
238
- it takes the same [--kind gate|live] [--external] [--strict] [--json]
239
- --all every proof; a \`perform\` one is LISTED, never started, so this
240
- never exits 5. ONE line per proof — a step transcript is what a
241
- single-proof run is for [--kind gate|live] [--external] include
242
- external proofs [--strict] make an unavailable fatal [--json]
243
- status workspace status: compiled runtime freshness, per-module channel/ref, staleness,
244
- and one \`proofs:\` line counting each proof's LAST verdict on this machine
245
- [--strict] exit 1 when any proof's ledger tail is a FAIL
246
- start serve the clean REST api at /api [--port <n>]
247
-
248
- containers — a workspace as a running container (Docker Engine API over its socket, no dependency;
249
- these verbs work with NO workspace, so npm i -g dreamteamer and Docker Desktop are enough):
250
- setup make THIS MACHINE ready: checks Docker, writes ~/.dreamteamer/.env with its defaults
251
- (DT_PORT_BASE 8100 · DT_BIND 127.0.0.1 · DT_REGISTRY · DT_TEMPLATE_TAG ·
252
- DT_DOCKER_TIMEOUT 30 — seconds a request to Docker may sit idle before the verb
253
- fails), lists the templates present, pulls one on request [--template <t>] [--json]
254
- start container <name> --template <t> create-if-absent and start: a code-server editor at
255
- http://localhost:<port>/ — the machine home — over a compiled workspace, three named
256
- volumes (workspace · home · files), its own bridge network dreamteamer-<name>, image
257
- <DT_REGISTRY>/<template>:<tag> or DT_IMAGE_<template>. The workspace is mounted at
258
- /workspaces/<name>. Idempotent. NO credential is ever injected — log in INSIDE, once;
259
- the home volume keeps it. An image with a URL token (hq 0.6+) prints the URL as
260
- ?tkn=<token>, read from the container; the token is never written on the host.
261
- [--rotate-token] replace the image's URL token and print the new URL
262
- [--workspace [<w>]] open /workspaces/<w> (default: its own) instead of the home
263
- [--repo <git url>] clone an EXISTING workspace into the volume on first start, instead
264
- of laying the template down — how a person joins one on GitHub
265
- [--mount <host-path|volume>:<container-path>[:ro]] extra mounts, repeatable; targets
266
- under /workspaces · /home/node · /files · /mnt, none at or under
267
- another mount's target, no bind source inside another
268
- [--name <git name>] [--email <git email>] [--no-open] [--json]
269
- stop container <name> stop it; every volume kept [--json]
270
- open container <name> print (and open) its URL, token included [--no-open] [--json]
271
- [--workspace [<w>]]
272
- [--vscode] print (and open) the Dev Containers attach URI instead — the host's own
273
- VS Code inside the container, extensions from the image's metadata label
274
- list containers | images the record verbs, answered over Docker instead of a
275
- get container <name> | image <ref> folder — singular or plural, either spelling.
276
- rm container <name> [--force] removes it and its network; keeps the volumes unless --force
277
- add image --template <t> pull a template's image; rm image <ref> removes one
278
- export container <name> --out <file> its WORKSPACES as one file — every folder under
279
- /workspaces, never the home or a login; node_modules and .files stay behind. Works on
280
- a stopped container. Encrypted with the owner passphrase (DT_EXPORT_PASSPHRASE, else a
281
- prompt — never a flag). [--workspace <w>]... only these [--no-encrypt] a plain .tar.gz
282
- [--json] (both verbs) the summary as JSON on stdout, every other line on stderr
283
- Secrets stay behind: .env and .env.* (not .example/.sample/.template), .envrc,
284
- .npmrc, .netrc, .git-credentials, .pypirc, .docker/config.json, and the credentials in
285
- each .git/config. [--with-secrets] carries them unchanged
286
- import container <name> <file> unpack an export into a RUNNING container, owned by
287
- node. Refuses a wrong passphrase, a damaged file, an entry leaving its workspace and a
288
- workspace already holding files — each before anything is written.
289
- [--workspace <w>]... only these [--replace] empty a workspace that holds files first
290
- [--as <name>] land the ONE workspace in /workspaces/<name> — e.g. another container's
291
- own volume folder (dt-new's name rule; never the container's own layer)
209
+ status workspace status: compiled runtime freshness, per-module channel/ref, staleness
292
210
 
293
211
  changes what changed in every repo that holds records, as record events
294
212
  [--since <sha|YYYY-MM-DD>] (default: HEAD~1 — the last commit's own changes) [--json]
@@ -298,17 +216,10 @@ these verbs work with NO workspace, so npm i -g dreamteamer and Docker Desktop a
298
216
  <collection> or one <collection>/<id> — the record form is what keeps a
299
217
  concurrent session's pending records out of your commit.
300
218
  [<collection>|<collection>/<id> …] [-m <subject>] [--dry-run] [--json]
301
- export render the workspace for a consumer that is not a coding agent, and optionally sync it
302
- export notebooklm [--out <dir>] [--plan standard|plus|pro|ultra|<n>] [--max-words <n>]
303
- [--collections a,b] [--instructions <template.md>]
304
- [--notebook <id> | --create "<title>"] [--response-length default|longer|shorter]
305
- [--mode default|learning-guide|concise|detailed] [--wait] [--json]
306
- writes one schema source (workspace → module → collection → field), one source per
307
- collection (sharded by --max-words, titled \`dt · <c> [n/m]\`), and the persona from a
308
- template; a collection marked \`sensitive: true\` and a field marked \`x-sensitive: true\`
309
- never travel. Refuses when the sources exceed the plan. With --notebook/--create it
310
- adds, replaces and removes its own sources to match and applies the persona.
311
219
  help this text
220
+
221
+ extension verbs — an extension (a workspace module, or a dependency, whose package.json declares
222
+ \`dreamteamer.extension\`) adds its own verbs, listed below when this workspace has any.
312
223
  `;
313
224
 
314
225
  // Record verbs, split by what their <target> means. `move` and `next` are in NEITHER set: both
@@ -344,31 +255,28 @@ export const GLOBAL_FLAGS = ['vault'];
344
255
  export const WORKSPACE_FLAGS = {
345
256
  init: ['name', 'data-path', 'harnesses', 'workspace-module'], update: [],
346
257
  install: ['clone', 'dry-run', 'json', 'link-env', 'all', 'hook', 'print-adapters'],
347
- // `start` is TWO forms: bare, the REST api (--port); with a `container <name>` target, the
348
- // lifecycle verb — whose flags are the driver's. One table, because `flags-honoured` reads it.
349
- start: ['port', ...CONTAINER_FLAGS], compile: ['watch'], check: [], status: ['strict'],
350
- setup: ['template', 'json'], stop: ['json'], open: ['json', 'no-open', 'vscode', 'workspace'],
351
- changes: ['since', 'json'], commit: ['dry-run', 'json'],
352
- export: EXPORT_FLAGS,
353
- // the UNION of every form's flags — the outer typo gate. Which flags each FORM takes is refused
354
- // inside `proveCommand`, where the target has been resolved against the compiled proofs.
355
- prove: ['all', 'kind', 'record', 'restart', 'json', 'keep', 'here', 'external', 'strict'],
356
- // same shape: the union of both forms, with `--hook`'s vocabulary refused in the arm below
357
- land: ['keep', 'dry-run', 'branch', 'hook', 'json'],
258
+ compile: ['watch'], check: [], status: [],
259
+ changes: ['since', 'json'], commit: ['dry-run', 'json'], relocate: ['dry-run', 'json', 'to-root'],
358
260
  };
359
261
 
360
- export function run(argv) {
262
+ /** Every verb this CLI answers itself — the set an extension's `commands` may not claim. The retired
263
+ * spellings are in it too: they answer with their replacement, and an extension taking one over
264
+ * would turn a loud translation into a different command. */
265
+ export const CORE_VERBS = [
266
+ 'init', 'install', 'update', 'compile', 'check', 'status', 'changes', 'commit', 'help', 'version', '--version', '-v',
267
+ 'list', 'add', 'values', 'get', 'set', 'rm', 'rename', 'history', 'diff', 'revert', 'move', 'next',
268
+ 'add-field', 'set-field', 'rm-field', 'rename-field', 'relations', 'resolve', 'relocate',
269
+ 'schema', 'ensure', 'update-field', 'remove-field', 'commands',
270
+ ];
271
+
272
+ /** Verbs that left core in 0.31.0. The extensions that will answer them are not published, so the old
273
+ * spelling in a script or a skill fails saying what happened — never "unknown verb", and never an
274
+ * install line for a package a stranger cannot install. */
275
+ const MOVED_VERBS = new Set(['prove', 'land', 'worktree', 'serve', 'notebooklm', 'start', 'export', 'setup', 'stop', 'open', 'import']);
276
+
277
+ export async function run(argv) {
361
278
  const [cmd, ...rest] = argv;
362
279
  try {
363
- // HOST VERBS resolve BEFORE workspace discovery: `setup`, and any verb whose target is a
364
- // driver collection (`containers`, `images`, singular or plural). They answer identically on a
365
- // bare machine — `npm i -g dreamteamer` and Docker Desktop, nothing else — and inside a
366
- // workspace, because the thing they make IS the workspace (src/containers.js).
367
- const host = hostDispatch(cmd, rest);
368
- if (host) {
369
- host.then((code) => process.exit(code)).catch((e) => { console.error(`✖ ${e.message}`); process.exit(1); });
370
- return;
371
- }
372
280
  if (cmd in WORKSPACE_FLAGS) {
373
281
  const bad = rest.filter((a) => a.startsWith('--')).map((a) => a.slice(2).split('=')[0]).find((f) => !WORKSPACE_FLAGS[cmd].includes(f));
374
282
  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)'}`);
@@ -386,12 +294,19 @@ export function run(argv) {
386
294
  process.exit(init({ flags }));
387
295
  }
388
296
  if (!cmd || cmd === 'help') {
389
- // `help` works OUTSIDE a workspace too — the host verbs above do, and a person who just ran
390
- // `npm i -g dreamteamer` on a bare machine has nothing else to read.
391
- emit(USAGE);
297
+ // `help` works OUTSIDE a workspace too — a person who just ran `npm i -g dreamteamer` on a
298
+ // bare machine has nothing else to read. Inside one, each installed extension's own usage
299
+ // follows the engine's.
300
+ let ws = null;
301
+ try { ws = await openWorkspace(); } catch { /* no workspace, or an extension that will not load — plain help */ }
302
+ emit(USAGE + extensionUsage(ws));
392
303
  process.exit(0);
393
304
  }
394
- const ws = findWorkspace();
305
+ const ws = await openWorkspace();
306
+ // An EXTENSION'S VERB runs in-process, handed this workspace — the same engine instance, the
307
+ // same loaded extensions. Its exit code is the process's.
308
+ const ext = ws.extensions.find((e) => cmd in e.commands);
309
+ if (ext) process.exit((await ext.commands[cmd].run(ws, rest)) ?? 0);
395
310
  switch (cmd) {
396
311
  // ONE verb makes a thing present and ready: this checkout, or an attached repo's
397
312
  // working tree. `ensure` was the second spelling of the same idea and is retired in the
@@ -435,57 +350,20 @@ export function run(argv) {
435
350
  // as a symlink placed by a verb whose whole job is to print.
436
351
  if (given.includes('--hook')) {
437
352
  refuse('dt install --hook', ['--hook']);
438
- process.exit(installCommand(ws, rest));
353
+ process.exit(await installCommand(ws, rest, { open: openWorkspace }));
439
354
  }
440
355
  if (given.includes('--print-adapters')) {
441
356
  refuse('dt install --print-adapters', ['--print-adapters']);
442
- process.exit(installCommand(ws, rest));
357
+ process.exit(await installCommand(ws, rest, { open: openWorkspace }));
443
358
  }
444
359
  refuse('dt install', ['--dry-run', '--json', '--link-env']);
445
- process.exit(installCommand(ws, rest));
446
- }
447
- // ⚠ THE EXIT CODE IS THE WHOLE POINT OF THIS VERB, so `proveCommand` RETURNS it and this
448
- // line is the only place it becomes a process exit. Six codes are a contract — 0 pass ·
449
- // 1 fail · 3 unavailable · 4 no-fixture · 5 an actor is owed a step · 6 vacuous — and a
450
- // `throw` inside `prove` becomes 1 through the shared catch below, which is right for
451
- // every refusal it makes (a target that names nothing, a resume with nothing to resume,
452
- // a stray flag of the other form): those are errors about the INVOCATION, not verdicts
453
- // about an artifact, and 2 is already spoken for by "you typed a verb that is gone".
454
- //
455
- // The per-form flag refusal lives in `proveCommand` rather than here, unlike `install`'s:
456
- // deciding which form was typed means resolving the target against the compiled proofs
457
- // and artifacts, and a second copy of that resolution in this file — purely to choose
458
- // which message to print — is the drift the comment above `case 'install':` describes.
459
- case 'prove':
460
- warnIfStale(ws.root);
461
- process.exit(proveCommand(ws, rest).code);
462
- // THE ONE MOVEMENT VERB (decision 309). Two forms and, like `install`'s, each refuses the
463
- // other's vocabulary here: the flag table can only say which flags `land` HAS, and a
464
- // `--hook --keep` accepted-and-dropped would promise a worktree kept by a form that is
465
- // always a dry run. A refusal or a conflict throws or returns 1; 2 stays what it is
466
- // everywhere else — "you typed a verb that is gone".
467
- case 'land': {
468
- const given = rest.filter((a) => a.startsWith('--'));
469
- const stray = given.filter((f) => !(given.includes('--hook') ? ['--hook', '--dry-run', '--json'] : ['--keep', '--dry-run', '--branch', '--json']).includes(f));
470
- if (stray.length) {
471
- const form = given.includes('--hook') ? 'dt land --hook' : 'dt land worktrees/<name>';
472
- throw new Error(`${stray.join(' ')} ${stray.length > 1 ? 'are not flags' : 'is not a flag'} of \`${form}\` — that form takes ${(given.includes('--hook') ? ['--hook', '--dry-run', '--json'] : ['--keep', '--dry-run', '--branch', '--json']).join(' ')}`);
473
- }
474
- warnIfStale(ws.root);
475
- process.exit(landCommand(ws, rest));
360
+ process.exit(await installCommand(ws, rest, { open: openWorkspace }));
476
361
  }
477
362
  case 'update': {
478
363
  const code = update(ws, rest.find((a) => !a.startsWith('--')));
479
364
  compile(ws); // pulled modules may carry new sources — prints its own summary
480
365
  process.exit(code);
481
366
  }
482
- case 'start': {
483
- warnIfStale(ws.root);
484
- const portIdx = rest.indexOf('--port');
485
- import('./server.js').then(({ startServer }) =>
486
- startServer(ws, { port: portIdx > -1 ? Number(rest[portIdx + 1]) : 8080 }));
487
- return; // keep the process alive
488
- }
489
367
  case 'compile': {
490
368
  const code = compile(ws);
491
369
  if (!rest.includes('--watch')) process.exit(code);
@@ -611,51 +489,13 @@ export function run(argv) {
611
489
  console.log(line);
612
490
  }
613
491
  // ⚠ WHICH CHECKOUT AM I. Everything `install` decides turns on this, and a linked
614
- // worktree is indistinguishable from the primary by eye — so it is stated, with the
615
- // count of sibling worktrees holding records and commits that are invisible from
616
- // here. Wrapped like every other block below: `status` is the command run when
617
- // things are already wrong, and it must still print.
492
+ // worktree is indistinguishable from the primary by eye — so it is stated. Wrapped like
493
+ // every other block below: `status` is the command run when things are already wrong.
618
494
  try {
619
495
  const co = describeCheckout(ws.root);
620
496
  console.log(`checkout: ${co.kind === 'primary' ? 'primary' : `linked worktree of ${co.primary}`}`);
621
- const wts = listWorktrees(ws).filter((w) => !w.primary);
622
- console.log(`worktrees: ${wts.length} · ${wts.filter((w) => w.dirtyRecords).length} with dirty records · ${wts.filter((w) => w.ahead).length} ahead`);
623
- } catch { /* not a git checkout — nothing to report about worktrees */ }
624
- // ⚠ WHAT THIS MACHINE HAS ACTUALLY PROVED. A ledger is per-machine and gitignored, so
625
- // this line cannot be derived from the repo — and it is the only place a FAIL from
626
- // last week surfaces without being asked for. The TAIL per proof, not every row: a
627
- // proof that failed on Monday and passed on Tuesday is passing. Wrapped like every
628
- // block here — an older runtime has no `proofs` descriptor, and status must still print.
629
- // ⚠ M5 — COUNTED AS THE WALK GOES, NEVER ASSIGNED AT THE END OF IT. `proofsFailed =
630
- // tally.FAIL` sat below the loop, inside the try — so a throw partway through (an
631
- // unreadable ledger, a record that will not parse) left the gate reading ZERO failures
632
- // and `--strict` exiting 0 BECAUSE the count broke. That is the one direction a gate
633
- // must never fail: a silent green bought with a swallowed exception.
634
- let proofsFailed = 0;
635
- try {
636
- const tally = { PASS: 0, FAIL: 0, UNAVAILABLE: 0 };
637
- let declared = 0; let never = 0; let other = 0;
638
- // ⚠ A SANDBOX NOTHING WILL COME BACK FOR. `.worktrees/` is gitignored, so a kept or
639
- // un-removable one accumulates in silence and the ledger is the only thing that
640
- // knows the directory exists. `kept` is not a row field: a terminal row with a
641
- // sandbox and NO removal attempted (`null`) is exactly what `--keep` leaves behind.
642
- // `existsSync` because a count nothing can clear is a lie.
643
- const left = new Set();
644
- for (const { id } of new Store(ws).readAll('proofs')) {
645
- declared++;
646
- const rows = readLedger(ws.root, id);
647
- const t = rows[rows.length - 1];
648
- if (!t) never++;
649
- else if (t.verdict in tally) { tally[t.verdict]++; if (t.verdict === 'FAIL') proofsFailed++; }
650
- else other++;
651
- for (const r of rows) {
652
- const behind = r.sandbox_removed === false || (r.verdict !== 'PENDING' && r.sandbox_removed === null);
653
- if (r.sandbox && behind && fs.existsSync(r.sandbox)) left.add(r.sandbox);
654
- }
655
- }
656
- console.log(`proofs: ${declared} declared · ${tally.PASS} passed · ${tally.FAIL} failed · ${tally.UNAVAILABLE} unavailable · ${never} never${other ? ` · ${other} other` : ''}`);
657
- if (left.size) console.log(` sandboxes left behind: ${left.size} — dt list worktrees`);
658
- } catch { /* no proofs descriptor compiled — nothing to report */ }
497
+ } catch { /* not a git checkout */ }
498
+ if (ws.extensions.length) console.log(`extensions: ${ws.extensions.map((e) => `${e.name}@${e.version}`).join(' · ')}`);
659
499
  console.log(`entries: ${Object.keys(s.manifest.entries).length}`);
660
500
  // repos materialize LAZILY, so presence is REPORTED here rather than stored on the
661
501
  // record. Wrapped: an older workspace may predate the repos descriptor, and status
@@ -686,45 +526,14 @@ export function run(argv) {
686
526
  process.exit(1);
687
527
  }
688
528
  console.log('✔ .dreamteamer is fresh');
689
- // ⚠ THE FAIL IS FATAL ONLY WHEN ASKED. `status` is the command you run when things are
690
- // already wrong, so it prints EVERYTHING first and gates last — the same shape the
691
- // staleness exit above has.
692
- // ⚠ R38/R46 — `--strict=true` IS THE SAME FLAG, AND `--strict=false` IS OFF. The
693
- // unknown-flag gate above splits on `=`, so the `=` spelling was ACCEPTED and then read
694
- // as "no --strict at all" — a CI step written that way stayed green over a failing
695
- // proof, for a reason nothing printed. `flagEnabled` is the one reader of that shape,
696
- // shared with `dt prove`, so the two verbs cannot disagree about what was typed.
697
- if (flagEnabled(rest, 'strict') && proofsFailed) {
698
- console.log(`✖ ${proofsFailed} proof(s) FAILED on this machine — dt list proofs`);
699
- process.exit(1);
700
- }
701
529
  process.exit(0);
702
530
  }
703
- // `export <target>` — the workspace rendered for a consumer that is not a coding agent. One
704
- // target today; the map in export-notebooklm.js is where a second one would register, the way
705
- // harnesses do. Without --notebook/--create it is a pure render and touches no network.
706
- case 'export': {
707
- warnIfStale(ws.root);
708
- const { flags, pos } = parseArgs(rest);
709
- process.exit(exportCommand(ws, pos[0], flags));
710
- }
711
531
  case 'help':
712
532
  emit(USAGE);
713
533
  process.exit(0);
714
534
  case 'list': case 'add': case 'values':
715
535
  case 'get': case 'set': case 'rm': case 'rename': case 'history': case 'diff': case 'revert':
716
536
  case 'move': case 'next': {
717
- // ⚠ `worktrees` IS NOT A COLLECTION — it is observed from git — so it is intercepted
718
- // here rather than being dispatched. Which means it never reaches
719
- // `collectionCommand`, where every other verb's flags are refused: the parse and the
720
- // refusal have to be done HERE or `--tmep` is swallowed and a request for a
721
- // throwaway sandbox silently becomes a permanent branch worktree.
722
- const target = rest[0];
723
- if (target === 'worktrees' || target?.startsWith('worktrees/')) {
724
- const { flags } = parseArgs(rest.slice(1));
725
- refuseUnknownFlags(null, 'worktrees', cmd, flags);
726
- process.exit(worktreeCommand(ws, cmd, target, flags));
727
- }
728
537
  warnIfStale(ws.root);
729
538
  process.exit(dispatchRecordVerb(ws, cmd, rest));
730
539
  }
@@ -749,6 +558,24 @@ export function run(argv) {
749
558
  case 'relations':
750
559
  warnIfStale(ws.root);
751
560
  process.exit(relationsCommand(ws, rest));
561
+ case 'relocate': {
562
+ warnIfStale(ws.root);
563
+ const store = new Store(ws);
564
+ const target = rest.find((a) => !a.startsWith('--'));
565
+ if (!target) throw new Error('dt relocate needs a target: dreamteamer relocate <collection> | <collection>/<id> [--dry-run]');
566
+ // a collection, or one record of it — the either-shape every other target has
567
+ const asCollection = canonicalCollection(store.descriptors, target);
568
+ const { collection, id } = asCollection ? { collection: asCollection, id: null } : splitRef(store.descriptors, target);
569
+ const out = store.relocate(collection, { only: id ? [id] : null, dryRun: rest.includes('--dry-run'), toRoot: rest.includes('--to-root') });
570
+ if (rest.includes('--json')) { emit(JSON.stringify(out, null, 2)); process.exit(out.problems.length ? 1 : 0); }
571
+ const rel = (p) => path.relative(ws.root, p);
572
+ for (const m of out.moves) console.log(`${out.applied ? '✔' : '→'} ${collection}/${m.id} ${rel(m.from)} → ${rel(m.to)}`);
573
+ for (const p of out.problems) console.error(`✖ ${p}`);
574
+ if (!out.moves.length && !out.problems.length) console.log(`nothing to relocate — every ${collection} record is where its descriptor puts it`);
575
+ else if (!out.applied && !out.problems.length) console.log(`${out.moves.length} move(s) planned (dry run) — nothing was moved`);
576
+ else if (out.applied) console.log(`${out.moves.length} record(s) relocated — ids and references unchanged; \`dreamteamer commit ${collection}\` publishes the moves`);
577
+ process.exit(out.problems.length ? 1 : 0);
578
+ }
752
579
  case 'resolve':
753
580
  process.exit(resolveVariables(ws, rest));
754
581
  default:
@@ -795,6 +622,10 @@ export function run(argv) {
795
622
  console.error(' dt list commands the command entities this workspace ships');
796
623
  process.exit(2);
797
624
  }
625
+ if (MOVED_VERBS.has(cmd)) {
626
+ console.error(`✖ \`dt ${cmd}\` left core in 0.31.0 and returns as an extension, which is not published yet — dreamteamer 0.30.x still has it, and a workspace module can carry its own verb (\`dreamteamer.extension\`)`);
627
+ process.exit(2);
628
+ }
798
629
  console.error(`✖ unknown verb "${cmd}" — dreamteamer is verb-first since 0.12.0: dt <verb> [<target>]`);
799
630
  emit(USAGE, 2);
800
631
  process.exit(1);
@@ -811,26 +642,11 @@ export function run(argv) {
811
642
  }
812
643
  }
813
644
 
814
- /** The verbs that run with no workspace. Returns a promise of an exit code, or null when the
815
- * command is not ours and the ordinary workspace dispatch should take it. */
816
- function hostDispatch(cmd, rest) {
817
- if (cmd === 'setup') {
818
- const bad = rest.filter((a) => a.startsWith('--')).map((a) => a.slice(2).split('=')[0]).find((f) => !WORKSPACE_FLAGS.setup.includes(f));
819
- if (bad) throw new Error(`unknown flag "--${bad}" on \`dt setup\`\n known: ${WORKSPACE_FLAGS.setup.map((f) => `--${f}`).join(', ')}`);
820
- return hostSetup(hostFlags(rest).flags);
821
- }
822
- const target = driverTarget(rest[0]);
823
- // `export container` is the driver's; `export notebooklm` stays the workspace verb below
824
- if (target && (cmd === 'export' || cmd === 'import')) return archiveCommand(cmd, target, rest.slice(1));
825
- if (cmd === 'import') return Promise.reject(new Error('dt import container <name> <file> [--workspace <w>]... [--replace]'));
826
- if (target && DRIVER_VERBS.has(cmd)) return driverCommand(cmd, target, rest.slice(1));
827
- // A lifecycle verb aimed at anything else is refused by name: `dt start tasks` is not a
828
- // server and not a container, and "unknown collection" would send the reader the wrong way.
829
- if (LIFECYCLE_VERBS.has(cmd) && rest[0] && !rest[0].startsWith('--')) {
830
- 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' : ''}`));
831
- }
832
- if (cmd === 'stop' || cmd === 'open') return Promise.reject(new Error(`dt ${cmd} container <name> — see \`dreamteamer help\``));
833
- return null;
645
+ /** Each installed extension's usage block, headed by its package — '' outside a workspace. */
646
+ function extensionUsage(ws) {
647
+ const blocks = (ws?.extensions ?? []).filter((e) => Object.keys(e.commands).length).map((e) =>
648
+ `\n${e.name}@${e.version}:\n${Object.entries(e.commands).map(([verb, c]) => (c.usage ?? ` ${verb}`).replace(/\s+$/, '')).join('\n')}`);
649
+ return blocks.length ? `\ninstalled extensions:${blocks.join('\n')}` : '';
834
650
  }
835
651
 
836
652
  /** Translate `dt <verb> <target> …` into the noun-verb call the implementation layer takes. */
@@ -940,7 +756,7 @@ function watchAndRecompile(ws) {
940
756
  };
941
757
  // 'system' plus the flat kinds: the classic layout can put sources at the workspace root under
942
758
  // either spelling, and a watcher that misses one makes --watch quietly stop recompiling.
943
- for (const dir of ['system', ...KINDS, 'modules', 'git_modules'].map((d) => path.join(ws.root, d))) {
759
+ for (const dir of ['system', ...kindsOf(ws), 'modules', 'git_modules'].map((d) => path.join(ws.root, d))) {
944
760
  if (fs.existsSync(dir)) fs.watch(dir, { recursive: true }, trigger);
945
761
  }
946
762
  }