dreamteamer 0.20.0 → 0.22.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/src/cli.js CHANGED
@@ -15,13 +15,17 @@ import { execFileSync } from 'node:child_process';
15
15
  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
- import { collectionCommand, emit, relationsCommand } from './collections-cli.js';
19
- import { init, install, installClone, update, listRepos } from './init.js';
18
+ import { collectionCommand, emit, relationsCommand, parseArgs, refuseUnknownFlags } from './collections-cli.js';
19
+ import { init, installClone, update, listRepos } from './init.js';
20
+ import { installCommand, describeCheckout, listWorktrees, worktreeCommand } from './checkout.js';
21
+ import { proveCommand, readLedger, flagEnabled } from './prove.js';
22
+ import { landCommand } from './land.js';
20
23
  import { deriveEvents } from './events.js';
21
24
  import { commitPending } from './commit.js';
22
25
  import { Store } from './store.js';
23
26
  import { splitRef } from './ref.js';
24
27
  import { envContext, renderTemplate } from './env-vars.js';
28
+ import { exportCommand, EXPORT_FLAGS } from './export-notebooklm.js';
25
29
 
26
30
  // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
27
31
  // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
@@ -35,6 +39,10 @@ record verbs (hard validation — invalid writes are rejected before disk).
35
39
  A <target> is either a collection name or a <collection>/<id> reference; the reference splits at
36
40
  the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee is ONE argument:
37
41
  list <collection> [--filter k=v] [--where <json>] [--sort [-]<field>] [--json]
42
+ list proofs [--missing] (a proof listing appends two COMPUTED columns:
43
+ availability on THIS machine, and the last verdict
44
+ from its ledger. --missing inverts it — one line per
45
+ artifact no proof is about, so it takes no filter)
38
46
  (--filter is ONE condition — repeat it to AND more
39
47
  (--filter a=1 --filter b=2 wants both);
40
48
  anything compound goes in one --where, operator
@@ -64,14 +72,12 @@ the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee i
64
72
  history <collection>/<id> [--json] (git revisions of this record, newest first)
65
73
  diff <collection>/<id> [--hash <sha>] (the patch one revision applied; defaults to HEAD)
66
74
  revert <collection>/<id> --hash <sha> (restore the content at <sha> as a pending write)
67
- commands <collection>[/<id>] [--ids <id>,…] (bound commands + per-record state:
68
- available / done / not-applicable)
75
+ next <collection>[/<id>] [--ids <id>,…] (which bound commands apply, and in which state:
76
+ available / done / not-applicable — what can
77
+ happen to this record next)
69
78
  relations [<collection>] (every two-way pair: owner.field → target.mirror)
70
79
  relations rebuild <collection> [--drop <f>] (regenerate mirror VALUES from the owning side;
71
80
  --drop removes a stale ex-mirror key from records)
72
- ensure <repos-id> | --all [--json] (materialize an attached repo's working tree ON
73
- DEMAND — never at install; --all is the explicit
74
- opt-in, e.g. before going offline)
75
81
  resolve '<string>' | <collection>/<id> <field>
76
82
  (render \${env:NAME} · \${workspaceFolder} ·
77
83
  \${userHome} — the ONLY substitution point; a
@@ -79,10 +85,11 @@ the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee i
79
85
  field prints one item per line)
80
86
 
81
87
  system verbs — the SAME verbs, on the entities the compiler materializes (modules, collections,
82
- skills, agents, commands, command-bindings, ui-views, collection-templates). ⚠ ONE difference in
83
- POLICY, not in spelling: a SYSTEM write commits itself, because an uncompilable or unpublished
84
- schema is not a state a workspace should sit in; a RECORD write does not — \`commit\` publishes it.
85
- The commit lands in the repo that holds the source, so a write into a git module commits there.
88
+ skills, agents, commands, command-bindings, ui-views, collection-templates, proofs). ⚠ ONE
89
+ difference in POLICY, not in spelling: a SYSTEM write commits itself, because an uncompilable or
90
+ unpublished schema is not a state a workspace should sit in; a RECORD write does not — \`commit\`
91
+ publishes it. The commit lands in the repo that holds the source, so a write into a git module
92
+ commits there.
86
93
  add collections --name <name> [--module <m>] [--namespace <ns>] [--template docs|entity]
87
94
  [--description "…"] [--suffix <s>] [--id-shape dated|slug]
88
95
  (--namespace health --name doctors === --name
@@ -122,7 +129,7 @@ field verbs — a field is the one sub-entity, and it has verbs of its own (ther
122
129
  collection: the ENGINE does not read one, and \`rename-field\` was the only capability it would buy):
123
130
  add-field <collection> --name <field> --type <type> [--options a,b] [--default-value v]
124
131
  [--required true] [--description "…"] [--many] [--inverse [name]]
125
- [--inverse-description "…"] [--unique] [--body] [--module <m>]
132
+ [--inverse-description "…"] [--unique] [--body] [--sensitive] [--module <m>]
126
133
  [--on-delete restrict|set-null] [--mirror-of <collection>.<field>]
127
134
  types: string text markdown boolean number integer date datetime
128
135
  enum tags <collection> — a date-time may be written as
@@ -135,8 +142,8 @@ collection: the ENGINE does not read one, and \`rename-field\` was the only capa
135
142
  target to have one.
136
143
  --module writes an OVERLAY in that module (it must declare the base's
137
144
  module in dreamteamer.dependencies).
138
- update-field <collection> --name <field> [--type <type>] [--options a,b] [--default-value v]
139
- [--required true|false] [--description "…"] [--body true|false]
145
+ set-field <collection> --name <field> [--type <type>] [--options a,b] [--default-value v]
146
+ [--required true|false] [--description "…"] [--body true|false] [--sensitive true|false]
140
147
  [--many] [--inverse [name]] [--unique] [--module <m>]
141
148
  [--on-delete restrict|set-null] [--mirror-of <collection>.<field>]
142
149
  (an existing description survives a retype, and so does every relation
@@ -145,7 +152,7 @@ collection: the ENGINE does not read one, and \`rename-field\` was the only capa
145
152
  restating --type. --inverse= drops the mirror; --unique false clears
146
153
  the one-to-one. Records written before the mirror existed are counted
147
154
  for you, with the "relations rebuild" that repairs them.)
148
- remove-field <collection> --name <field> [--module <m>] [--dry-run]
155
+ rm-field <collection> --name <field> [--module <m>] [--dry-run]
149
156
  (clears the field's VALUES in the same write, and reports the count)
150
157
  rename-field <collection> --name <field> --to <new-name> [--module <m>] [--dry-run]
151
158
  (rewrites the key in every record AND everywhere a descriptor or view
@@ -159,13 +166,66 @@ Every verb that MOVES records or CLEARS values takes --dry-run and prints its pl
159
166
  workspace verbs:
160
167
  init write the workspace skeleton into the current directory (never compiles)
161
168
  --version print the engine version (works anywhere)
162
- install restore git_modules/ from the lockfile map; --clone <url> [name] adds one
169
+ install make THIS checkout ready — the engine, .env (linked from the primary when this is a
170
+ worktree), declared local assets, git modules, compile, and a declared postinstall.
171
+ Idempotent: it prints a board of what it found and what it did
172
+ [--dry-run] plan only [--json] the board as data
173
+ [--link-env] link .env even into a worktree OUTSIDE the primary root
174
+ install --hook | --print-adapters
175
+ --hook: read a harness hook's JSON payload from stdin and install the checkout its
176
+ \`cwd\` names — the process cwd is the harness's, never the worktree's. In a linked
177
+ worktree the board's last line is the landing instruction.
178
+ --print-adapters: print the hook snippet for each declared harness. Merging it into
179
+ the harness's own settings file is the operator's act — the engine never writes one
180
+ install repos/<id> | repos --all [--json]
181
+ materialize an attached repo's working tree ON DEMAND — never as part of making a
182
+ checkout ready; --all is the explicit opt-in, e.g. before going offline
183
+ install --clone <url> [name] attach a git module to this workspace
184
+ add worktrees --name <n> [--path <dir>] [--base <ref>] [--temp] | --hook
185
+ cut a linked git worktree on branch worktree-<n> and \`install\` it, so it is ready
186
+ to work in; it prints its absolute path LAST, which is what a creation hook echoes.
187
+ --temp: detached, no branch, under .worktrees/.tmp-* inside this root — a sandbox
188
+ --hook: take the name from a WorktreeCreate payload on stdin; implies .worktrees/<n>
189
+ list worktrees | get worktrees/<n> | rm worktrees/<n> [--force]
190
+ observed from \`git worktree list\`, never stored. rm refuses a worktree holding
191
+ dirty records or commits not on the primary branch — neither is visible from here
192
+ land worktrees/<name|path> [--keep] [--dry-run] [--branch <n>] [--json]
193
+ land a worktree's commits onto the primary branch — the one MOVEMENT verb. It refuses
194
+ while the primary holds pending record writes, rebases a COPY of the branch under one
195
+ engine-owned lock, resolves ONLY the generated harness block, fast-forwards, recompiles
196
+ the primary, then removes the worktree and its branch. A conflict in a record aborts
197
+ and leaves every tree exactly as it was
198
+ [--keep] keep the worktree, reset onto what landed
199
+ [--branch <n>] give a DETACHED worktree the branch worktree-<n> first
200
+ [--dry-run] print the plan and change nothing
201
+ land --hook [--dry-run] [--json]
202
+ read a WorktreeRemove payload on stdin and report what that worktree still holds.
203
+ ALWAYS a dry run — the engine never lands while a harness is deleting the tree
163
204
  update pull git_modules clones forward (ff-only on the lockfile ref), rebuild,
164
205
  then compile; [<name>] updates just one. dirty clones are skipped
165
206
  compile materialize modules + workspace sources into .dreamteamer (+ harness adapters)
166
207
  [--watch] recompile on source changes
167
208
  check validate every record against the compiled descriptors (report-only)
168
- status workspace status: compiled runtime freshness, per-module channel/ref, staleness
209
+ prove <proof> | <artifact-ref> | --all
210
+ run a proof and answer with an EXIT CODE: 0 pass · 1 fail · 3 unavailable (this
211
+ machine lacks a required var or binary) · 4 no-fixture (the given matched no record)
212
+ · 5 a \`perform\` step is owed a human/agent · 6 vacuous (every expectation already
213
+ held, so the proof cannot fail). Evidence lands in .dreamteamer/.proofs/<id>.jsonl
214
+ <proof> [--record <c>/<id>] finish the pending run for that record
215
+ [--restart] discard a pending run and start over
216
+ [--here] run a \`writes\` proof in this workspace, not a sandbox
217
+ [--keep] keep the sandbox afterwards — under --json that is
218
+ \`kept: true\` beside the \`sandbox\` path [--json] one object on stdout
219
+ <artifact-ref> every proof whose \`about\` names skills/<id>, commands/<id>,
220
+ command-bindings/<id> or <module>/bin/<file> — board semantics, so
221
+ it takes the same [--kind gate|live] [--external] [--strict] [--json]
222
+ --all every proof; a \`perform\` one is LISTED, never started, so this
223
+ never exits 5. ONE line per proof — a step transcript is what a
224
+ single-proof run is for [--kind gate|live] [--external] include
225
+ external proofs [--strict] make an unavailable fatal [--json]
226
+ status workspace status: compiled runtime freshness, per-module channel/ref, staleness,
227
+ and one \`proofs:\` line counting each proof's LAST verdict on this machine
228
+ [--strict] exit 1 when any proof's ledger tail is a FAIL
169
229
  start serve the clean REST api at /api [--port <n>]
170
230
  changes what changed in every repo that holds records, as record events
171
231
  [--since <sha|YYYY-MM-DD>] (default: HEAD~1 — the last commit's own changes) [--json]
@@ -175,10 +235,20 @@ workspace verbs:
175
235
  <collection> or one <collection>/<id> — the record form is what keeps a
176
236
  concurrent session's pending records out of your commit.
177
237
  [<collection>|<collection>/<id> …] [-m <subject>] [--dry-run] [--json]
238
+ export render the workspace for a consumer that is not a coding agent, and optionally sync it
239
+ export notebooklm [--out <dir>] [--plan standard|plus|pro|ultra|<n>] [--max-words <n>]
240
+ [--collections a,b] [--instructions <template.md>]
241
+ [--notebook <id> | --create "<title>"] [--response-length default|longer|shorter]
242
+ [--mode default|learning-guide|concise|detailed] [--wait] [--json]
243
+ writes one schema source (workspace → module → collection → field), one source per
244
+ collection (sharded by --max-words, titled \`dt · <c> [n/m]\`), and the persona from a
245
+ template; a collection marked \`sensitive: true\` and a field marked \`x-sensitive: true\`
246
+ never travel. Refuses when the sources exceed the plan. With --notebook/--create it
247
+ adds, replaces and removes its own sources to match and applies the persona.
178
248
  help this text
179
249
  `;
180
250
 
181
- // Record verbs, split by what their <target> means. `move` and `commands` are in NEITHER set: both
251
+ // Record verbs, split by what their <target> means. `move` and `next` are in NEITHER set: both
182
252
  // accept either shape, and which one it is has to be decided against the declared collections.
183
253
  // How many rows a per-record listing prints before it summarises. ONE number, because a report that
184
254
  // caps one of its lists and not the next reads as a bug in whichever list ran long.
@@ -186,22 +256,29 @@ const ROWS_SHOWN = 20;
186
256
 
187
257
  const REF_VERBS = new Set(['get', 'set', 'rm', 'rename', 'history', 'diff', 'revert']);
188
258
  const COLLECTION_VERBS = new Set(['list', 'add', 'values']);
189
- const EITHER_VERBS = new Set(['move', 'commands']);
259
+ const EITHER_VERBS = new Set(['move', 'next']);
190
260
 
191
261
  // FIELD VERBS. Their <target> is a collection and everything else is flags, which is the one shape
192
262
  // that differs from the record verbs — so they get their own case arm rather than being folded into
193
263
  // `dispatchRecordVerb`. There is no `schema <op>` table any more: system entities take the record
194
264
  // verbs, and `collectionCommand`'s interceptors are the whole dispatch (§4).
195
- const FIELD_VERBS = ['add-field', 'update-field', 'remove-field', 'rename-field'];
265
+ const FIELD_VERBS = ['add-field', 'set-field', 'rm-field', 'rename-field'];
196
266
 
197
267
  // WORKSPACE VERBS take a closed set of options, and nothing downstream of here would notice a
198
268
  // misspelling — `dt commit --dryrun` COMMITTED, because `rest.includes('--dry-run')` is false for a
199
269
  // flag nobody typed correctly. The record/system/field verbs are checked in `collections-cli.js`,
200
270
  // beside the parser they share; these nine have no shared parser, so the table is here.
201
271
  export const WORKSPACE_FLAGS = {
202
- init: ['name', 'data-path', 'harnesses', 'workspace-module'], install: ['clone'], update: [],
203
- start: ['port'], compile: ['watch'], check: [], status: [],
272
+ init: ['name', 'data-path', 'harnesses', 'workspace-module'], update: [],
273
+ install: ['clone', 'dry-run', 'json', 'link-env', 'all', 'hook', 'print-adapters'],
274
+ start: ['port'], compile: ['watch'], check: [], status: ['strict'],
204
275
  changes: ['since', 'json'], commit: ['dry-run', 'json'],
276
+ export: EXPORT_FLAGS,
277
+ // the UNION of every form's flags — the outer typo gate. Which flags each FORM takes is refused
278
+ // inside `proveCommand`, where the target has been resolved against the compiled proofs.
279
+ prove: ['all', 'kind', 'record', 'restart', 'json', 'keep', 'here', 'external', 'strict'],
280
+ // same shape: the union of both forms, with `--hook`'s vocabulary refused in the arm below
281
+ land: ['keep', 'dry-run', 'branch', 'hook', 'json'],
205
282
  };
206
283
 
207
284
  export function run(argv) {
@@ -229,10 +306,86 @@ export function run(argv) {
229
306
  }
230
307
  const ws = findWorkspace();
231
308
  switch (cmd) {
309
+ // ONE verb makes a thing present and ready: this checkout, or an attached repo's
310
+ // working tree. `ensure` was the second spelling of the same idea and is retired in the
311
+ // `default` arm below — no alias, per the 0.12.0 policy.
312
+ //
313
+ // ⚠ THREE FORMS, ONE FLAG TABLE — so each form REFUSES the other's vocabulary itself.
314
+ // `WORKSPACE_FLAGS.install` can only say which flags the verb has; it cannot know that
315
+ // `--dry-run` is meaningless once a target is named. Forwarding just the flags a form
316
+ // understands DROPS the rest in silence, and that is not a cosmetic loss:
317
+ // `dt install repos/x --dry-run` materialized the repo for real, driven past a flag
318
+ // whose entire meaning is "do nothing". A bare positional had the same shape of bug —
319
+ // `dt install <repo-id>`, the muscle memory `dt ensure <id>` taught for nine releases,
320
+ // matched neither branch, so the target was ignored and a whole checkout install ran.
321
+ // Same policy as the retired verbs below: a stale invocation fails loudly.
232
322
  case 'install': {
323
+ const given = rest.filter((a) => a.startsWith('--'));
324
+ const refuse = (form, allowed) => {
325
+ const stray = given.filter((f) => !allowed.includes(f));
326
+ if (!stray.length) return;
327
+ throw new Error(`${stray.join(' ')} ${stray.length > 1 ? 'are not flags' : 'is not a flag'} of \`${form}\` — that form takes ${allowed.join(' ')}`);
328
+ };
233
329
  const ci = rest.indexOf('--clone');
234
- if (ci > -1) process.exit(installClone(ws, rest[ci + 1], rest[ci + 2]));
235
- process.exit(install(ws));
330
+ if (ci > -1) {
331
+ refuse('dt install --clone <url> [name]', ['--clone']);
332
+ process.exit(installClone(ws, rest[ci + 1], rest[ci + 2]));
333
+ }
334
+ const target = rest.find((a) => !a.startsWith('--'));
335
+ if (target === 'repos' || target?.startsWith('repos/')) {
336
+ refuse(`dt install ${target}`, ['--all', '--json']);
337
+ warnIfStale(ws.root);
338
+ const args = target === 'repos' ? given : [target.slice('repos/'.length), ...given];
339
+ process.exit(collectionCommand(ws, 'repos', 'ensure', args));
340
+ }
341
+ if (target) {
342
+ throw new Error(`dt install takes no target "${target}" — \`dt install\` makes THIS checkout ready, and \`dt install repos/${target}\` materializes an attached repo`);
343
+ }
344
+ // ⚠ TWO MORE FORMS, EACH ITS OWN VOCABULARY. `--hook` reads a JSON payload off stdin
345
+ // and installs the checkout that payload names; `--print-adapters` installs nothing
346
+ // at all. Folding either into `dt install`'s flag list would let `--hook --dry-run`
347
+ // through as a plan of a checkout nobody named, and `--print-adapters --link-env`
348
+ // as a symlink placed by a verb whose whole job is to print.
349
+ if (given.includes('--hook')) {
350
+ refuse('dt install --hook', ['--hook']);
351
+ process.exit(installCommand(ws, rest));
352
+ }
353
+ if (given.includes('--print-adapters')) {
354
+ refuse('dt install --print-adapters', ['--print-adapters']);
355
+ process.exit(installCommand(ws, rest));
356
+ }
357
+ refuse('dt install', ['--dry-run', '--json', '--link-env']);
358
+ process.exit(installCommand(ws, rest));
359
+ }
360
+ // ⚠ THE EXIT CODE IS THE WHOLE POINT OF THIS VERB, so `proveCommand` RETURNS it and this
361
+ // line is the only place it becomes a process exit. Six codes are a contract — 0 pass ·
362
+ // 1 fail · 3 unavailable · 4 no-fixture · 5 an actor is owed a step · 6 vacuous — and a
363
+ // `throw` inside `prove` becomes 1 through the shared catch below, which is right for
364
+ // every refusal it makes (a target that names nothing, a resume with nothing to resume,
365
+ // a stray flag of the other form): those are errors about the INVOCATION, not verdicts
366
+ // about an artifact, and 2 is already spoken for by "you typed a verb that is gone".
367
+ //
368
+ // The per-form flag refusal lives in `proveCommand` rather than here, unlike `install`'s:
369
+ // deciding which form was typed means resolving the target against the compiled proofs
370
+ // and artifacts, and a second copy of that resolution in this file — purely to choose
371
+ // which message to print — is the drift the comment above `case 'install':` describes.
372
+ case 'prove':
373
+ warnIfStale(ws.root);
374
+ process.exit(proveCommand(ws, rest).code);
375
+ // THE ONE MOVEMENT VERB (decision 309). Two forms and, like `install`'s, each refuses the
376
+ // other's vocabulary here: the flag table can only say which flags `land` HAS, and a
377
+ // `--hook --keep` accepted-and-dropped would promise a worktree kept by a form that is
378
+ // always a dry run. A refusal or a conflict throws or returns 1; 2 stays what it is
379
+ // everywhere else — "you typed a verb that is gone".
380
+ case 'land': {
381
+ const given = rest.filter((a) => a.startsWith('--'));
382
+ const stray = given.filter((f) => !(given.includes('--hook') ? ['--hook', '--dry-run', '--json'] : ['--keep', '--dry-run', '--branch', '--json']).includes(f));
383
+ if (stray.length) {
384
+ const form = given.includes('--hook') ? 'dt land --hook' : 'dt land worktrees/<name>';
385
+ 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(' ')}`);
386
+ }
387
+ warnIfStale(ws.root);
388
+ process.exit(landCommand(ws, rest));
236
389
  }
237
390
  case 'update': {
238
391
  const code = update(ws, rest.find((a) => !a.startsWith('--')));
@@ -360,6 +513,52 @@ export function run(argv) {
360
513
  if (sh) line += ` — shadows ${CHANNEL_LABEL[sh.loser]} copy`;
361
514
  console.log(line);
362
515
  }
516
+ // ⚠ WHICH CHECKOUT AM I. Everything `install` decides turns on this, and a linked
517
+ // worktree is indistinguishable from the primary by eye — so it is stated, with the
518
+ // count of sibling worktrees holding records and commits that are invisible from
519
+ // here. Wrapped like every other block below: `status` is the command run when
520
+ // things are already wrong, and it must still print.
521
+ try {
522
+ const co = describeCheckout(ws.root);
523
+ console.log(`checkout: ${co.kind === 'primary' ? 'primary' : `linked worktree of ${co.primary}`}`);
524
+ const wts = listWorktrees(ws).filter((w) => !w.primary);
525
+ console.log(`worktrees: ${wts.length} · ${wts.filter((w) => w.dirtyRecords).length} with dirty records · ${wts.filter((w) => w.ahead).length} ahead`);
526
+ } catch { /* not a git checkout — nothing to report about worktrees */ }
527
+ // ⚠ WHAT THIS MACHINE HAS ACTUALLY PROVED. A ledger is per-machine and gitignored, so
528
+ // this line cannot be derived from the repo — and it is the only place a FAIL from
529
+ // last week surfaces without being asked for. The TAIL per proof, not every row: a
530
+ // proof that failed on Monday and passed on Tuesday is passing. Wrapped like every
531
+ // block here — an older runtime has no `proofs` descriptor, and status must still print.
532
+ // ⚠ M5 — COUNTED AS THE WALK GOES, NEVER ASSIGNED AT THE END OF IT. `proofsFailed =
533
+ // tally.FAIL` sat below the loop, inside the try — so a throw partway through (an
534
+ // unreadable ledger, a record that will not parse) left the gate reading ZERO failures
535
+ // and `--strict` exiting 0 BECAUSE the count broke. That is the one direction a gate
536
+ // must never fail: a silent green bought with a swallowed exception.
537
+ let proofsFailed = 0;
538
+ try {
539
+ const tally = { PASS: 0, FAIL: 0, UNAVAILABLE: 0 };
540
+ let declared = 0; let never = 0; let other = 0;
541
+ // ⚠ A SANDBOX NOTHING WILL COME BACK FOR. `.worktrees/` is gitignored, so a kept or
542
+ // un-removable one accumulates in silence and the ledger is the only thing that
543
+ // knows the directory exists. `kept` is not a row field: a terminal row with a
544
+ // sandbox and NO removal attempted (`null`) is exactly what `--keep` leaves behind.
545
+ // `existsSync` because a count nothing can clear is a lie.
546
+ const left = new Set();
547
+ for (const { id } of new Store(ws).readAll('proofs')) {
548
+ declared++;
549
+ const rows = readLedger(ws.root, id);
550
+ const t = rows[rows.length - 1];
551
+ if (!t) never++;
552
+ else if (t.verdict in tally) { tally[t.verdict]++; if (t.verdict === 'FAIL') proofsFailed++; }
553
+ else other++;
554
+ for (const r of rows) {
555
+ const behind = r.sandbox_removed === false || (r.verdict !== 'PENDING' && r.sandbox_removed === null);
556
+ if (r.sandbox && behind && fs.existsSync(r.sandbox)) left.add(r.sandbox);
557
+ }
558
+ }
559
+ console.log(`proofs: ${declared} declared · ${tally.PASS} passed · ${tally.FAIL} failed · ${tally.UNAVAILABLE} unavailable · ${never} never${other ? ` · ${other} other` : ''}`);
560
+ if (left.size) console.log(` sandboxes left behind: ${left.size} — dt list worktrees`);
561
+ } catch { /* no proofs descriptor compiled — nothing to report */ }
363
562
  console.log(`entries: ${Object.keys(s.manifest.entries).length}`);
364
563
  // repos materialize LAZILY, so presence is REPORTED here rather than stored on the
365
564
  // record. Wrapped: an older workspace may predate the repos descriptor, and status
@@ -370,8 +569,8 @@ export function run(argv) {
370
569
  const here = repos.filter((r) => r.present).length;
371
570
  console.log(`repos: ${here}/${repos.length} materialized`);
372
571
  // an UNRESOLVED path is not the same absence as a repo simply not cloned yet, and
373
- // `dreamteamer ensure` is not the fix for it — say which one this is.
374
- for (const r of repos) if (!r.present) console.log(` absent: ${r.id} → ${r.path}${r.unresolved ? ` — ${r.unresolved}` : ` (dreamteamer ensure ${r.id})`}`);
572
+ // `dreamteamer install repos/<id>` is not the fix for it — say which one this is.
573
+ for (const r of repos) if (!r.present) console.log(` absent: ${r.id} → ${r.path}${r.unresolved ? ` — ${r.unresolved}` : ` (dreamteamer install repos/${r.id})`}`);
375
574
  }
376
575
  } catch { /* no repos descriptor compiled — nothing to report */ }
377
576
  // Uncommitted records are invisible to `dt changes` (it diffs commits), so the
@@ -390,25 +589,58 @@ export function run(argv) {
390
589
  process.exit(1);
391
590
  }
392
591
  console.log('✔ .dreamteamer is fresh');
592
+ // ⚠ THE FAIL IS FATAL ONLY WHEN ASKED. `status` is the command you run when things are
593
+ // already wrong, so it prints EVERYTHING first and gates last — the same shape the
594
+ // staleness exit above has.
595
+ // ⚠ R38/R46 — `--strict=true` IS THE SAME FLAG, AND `--strict=false` IS OFF. The
596
+ // unknown-flag gate above splits on `=`, so the `=` spelling was ACCEPTED and then read
597
+ // as "no --strict at all" — a CI step written that way stayed green over a failing
598
+ // proof, for a reason nothing printed. `flagEnabled` is the one reader of that shape,
599
+ // shared with `dt prove`, so the two verbs cannot disagree about what was typed.
600
+ if (flagEnabled(rest, 'strict') && proofsFailed) {
601
+ console.log(`✖ ${proofsFailed} proof(s) FAILED on this machine — dt list proofs`);
602
+ process.exit(1);
603
+ }
393
604
  process.exit(0);
394
605
  }
606
+ // `export <target>` — the workspace rendered for a consumer that is not a coding agent. One
607
+ // target today; the map in export-notebooklm.js is where a second one would register, the way
608
+ // harnesses do. Without --notebook/--create it is a pure render and touches no network.
609
+ case 'export': {
610
+ warnIfStale(ws.root);
611
+ const { flags, pos } = parseArgs(rest);
612
+ process.exit(exportCommand(ws, pos[0], flags));
613
+ }
395
614
  case 'help':
396
615
  emit(USAGE);
397
616
  process.exit(0);
398
617
  case 'list': case 'add': case 'values':
399
618
  case 'get': case 'set': case 'rm': case 'rename': case 'history': case 'diff': case 'revert':
400
- case 'move': case 'commands':
619
+ case 'move': case 'next': {
620
+ // ⚠ `worktrees` IS NOT A COLLECTION — it is observed from git — so it is intercepted
621
+ // here rather than being dispatched. Which means it never reaches
622
+ // `collectionCommand`, where every other verb's flags are refused: the parse and the
623
+ // refusal have to be done HERE or `--tmep` is swallowed and a request for a
624
+ // throwaway sandbox silently becomes a permanent branch worktree.
625
+ const target = rest[0];
626
+ if (target === 'worktrees' || target?.startsWith('worktrees/')) {
627
+ const { flags } = parseArgs(rest.slice(1));
628
+ refuseUnknownFlags(null, 'worktrees', cmd, flags);
629
+ process.exit(worktreeCommand(ws, cmd, target, flags));
630
+ }
401
631
  warnIfStale(ws.root);
402
632
  process.exit(dispatchRecordVerb(ws, cmd, rest));
633
+ }
403
634
  // The verb `check`'s stale-mirror message names. It reads the compiled relations, and
404
635
  // rebuild WRITES records, so both want the same staleness warning every record verb gets.
405
636
  // FIELD VERBS — see FIELD_VERBS. Their <target> is a collection and everything else is
406
637
  // flags, so they are their own case rather than being folded into `dispatchRecordVerb`.
407
638
  //
408
- // ⚠ `dt schema <op>` is GONE, not aliased. The 0.12.0 policy: a stale invocation must fail
409
- // loudly, because a half-working grammar teaches the wrong shape without ever saying so.
410
- // The `default` arm below names `schema` specifically.
411
- case 'add-field': case 'update-field': case 'remove-field': case 'rename-field': {
639
+ // ⚠ THE RETIRED SPELLINGS ARE GONE, not aliased. The 0.12.0 policy: a stale invocation
640
+ // must fail loudly, because a half-working grammar teaches the wrong shape without ever
641
+ // saying so. The `default` arm below names each one — `schema`, `ensure`, `update-field`,
642
+ // `remove-field`, `commands` — and carries its replacement.
643
+ case 'add-field': case 'set-field': case 'rm-field': case 'rename-field': {
412
644
  warnIfStale(ws.root);
413
645
  const [target, ...flagArgs] = rest;
414
646
  if (!target || target.startsWith('--')) {
@@ -419,28 +651,51 @@ export function run(argv) {
419
651
  case 'relations':
420
652
  warnIfStale(ws.root);
421
653
  process.exit(relationsCommand(ws, rest));
422
- // `repos ensure` lost its noun: the repos collection is still where the declaration lives,
423
- // but materializing one is a verb the operator types, not a record write.
424
- case 'ensure':
425
- warnIfStale(ws.root);
426
- process.exit(collectionCommand(ws, 'repos', 'ensure', rest));
427
654
  case 'resolve':
428
655
  process.exit(resolveVariables(ws, rest));
429
656
  default:
430
657
  // ⚠ NAMED, not just unknown. Every doc, skill and downstream script spelled these
431
658
  // `dt schema <op>` for seven releases, so the failure has to carry the translation —
432
- // an "unknown verb" alone sends the reader to `help` to guess which of nine verbs
659
+ // an "unknown verb" alone sends the reader to `help` to guess which of thirty verbs
433
660
  // replaced the one they typed. No alias layer and no deprecation window: 0.12.0's
434
661
  // policy, and the reason it is the right one is that `dt contacts list` failing
435
662
  // loudly is what taught the verb-first grammar in one command.
663
+ //
664
+ // ⚠ ONE EXIT CODE FOR ALL OF THEM: 2, the usage code. "You typed a verb that is
665
+ // gone" is one answer, so a script (and an operator) asks it once rather than
666
+ // learning which retirement exits 1 and which exits 2.
436
667
  if (cmd === 'schema') {
437
668
  console.error('✖ unknown verb "schema" — schema verbs are gone since 0.19.0. System entities take the RECORD verbs now:');
438
669
  console.error(' dt add collections --name <c> [--module <m>] · dt rm collections/<c> · dt rename collections/<old> <new>');
439
670
  console.error(' dt set collections/<c> module=<m> | <scalar>=<v> · dt get collections/<c> [--module <m>]');
440
- console.error(' dt add-field <c> … · dt update-field <c> … · dt remove-field <c> … · dt rename-field <c> --name <f> --to <g>');
671
+ console.error(' dt add-field <c> … · dt set-field <c> … · dt rm-field <c> … · dt rename-field <c> --name <f> --to <g>');
441
672
  console.error(' dt add|set|rm|rename modules/<id> … · dt add|set|rm|rename ui-views/<id> …');
442
673
  console.error(' the full mapping table is in UPDATING.md (0.18.0 → 0.19.0), and `dt help` has the current spellings.');
443
- process.exit(1);
674
+ process.exit(2);
675
+ }
676
+ if (cmd === 'ensure') {
677
+ console.error('✖ unknown verb "ensure" — gone since 0.22.0 (decision 309: install is the one verb that makes a thing present and ready):');
678
+ console.error(' dt install this checkout — engine, .env, local assets, git modules, compile, postinstall');
679
+ console.error(' dt install repos/<id> materialize one attached repo · dt install repos --all');
680
+ process.exit(2);
681
+ }
682
+ // The change verbs spell one way. `add · set · rm · rename` are the record verbs, so
683
+ // `update-field`/`remove-field` were a second spelling for one action inside one
684
+ // grammar — the field verbs now say `add-field · set-field · rm-field · rename-field`.
685
+ if (cmd === 'update-field' || cmd === 'remove-field') {
686
+ const to = cmd === 'update-field' ? 'set-field' : 'rm-field';
687
+ console.error(`✖ unknown verb "${cmd}" — gone since 0.22.0 (decision 309: a field verb spells its action the way the record verbs do):`);
688
+ console.error(` dt ${to} <collection> --name <field> …`);
689
+ console.error(' the field verbs are: dt add-field · dt set-field · dt rm-field · dt rename-field');
690
+ process.exit(2);
691
+ }
692
+ // `dt commands <ref>` and `dt list commands` were one word for two things — the read
693
+ // verb and the system entity. `next` also says what it answers.
694
+ if (cmd === 'commands') {
695
+ console.error('✖ unknown verb "commands" — gone since 0.22.0 (decision 309: it collided with the `commands` ENTITY, which `dt list commands` reads):');
696
+ console.error(' dt next <collection>[/<id>] which bound commands apply to this record, and in which state');
697
+ console.error(' dt list commands the command entities this workspace ships');
698
+ process.exit(2);
444
699
  }
445
700
  console.error(`✖ unknown verb "${cmd}" — dreamteamer is verb-first since 0.12.0: dt <verb> [<target>]`);
446
701
  emit(USAGE, 2);
@@ -448,6 +703,12 @@ export function run(argv) {
448
703
  }
449
704
  } catch (e) {
450
705
  console.error(`✖ ${e.message}`);
706
+ // ⚠ AND ON STDOUT UNDER `--hook`, because that is the whole reason the shim exists.
707
+ // `bin/dt-hook.sh` fails onto stdout with a comment saying why: a hook's stdout is added to
708
+ // the session's context and its stderr is nobody's problem. Every success path here already
709
+ // prints to stdout; every FAILURE went to stderr alone, so a mis-wired hook, a bad payload, a
710
+ // TTY invocation or a refusal was invisible to exactly the reader the shim was built for.
711
+ if (process.argv.includes('--hook')) console.log(`✖ ${e.message}`);
451
712
  process.exit(1);
452
713
  }
453
714
  }
@@ -465,7 +726,7 @@ function dispatchRecordVerb(ws, verb, args) {
465
726
  return collectionCommand(ws, collection, verb, [id, ...rest]);
466
727
  }
467
728
  // EITHER_VERBS from here: a bare collection is legal for both — `move <collection> --init`,
468
- // `commands <collection>`.
729
+ // `next <collection>`.
469
730
  const { descriptors } = new Store(ws);
470
731
  if (descriptors.has(target)) {
471
732
  return verb === 'move'