dreamteamer 0.25.1 → 0.26.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.
@@ -12,6 +12,34 @@ import { pathToFileURL, fileURLToPath } from 'node:url';
12
12
  const MODULE_SEGMENTS = new Set(['node_modules', 'git_modules', 'modules']);
13
13
 
14
14
  const self = fileURLToPath(import.meta.url);
15
+
16
+ // `--vault <path>` — operate on ANOTHER workspace without leaving this one. Consumed HERE, before
17
+ // anything else resolves, because every verb downstream answers "which workspace am I in" from
18
+ // process.cwd(): changing the directory once IS the whole implementation, and it also points the
19
+ // dev-clone walk below at the TARGET's git_modules rather than the caller's.
20
+ //
21
+ // ⚠ It is spliced out of process.argv, not just out of the slice handed to run(): the dev-clone
22
+ // branch re-enters this same file, and a flag left in place would be applied a SECOND time — the
23
+ // path resolving against the already-changed directory, i.e. at the wrong workspace, silently.
24
+ //
25
+ // The path resolves against the INVOKING directory, which is where the caller typed it. Every other
26
+ // relative path in the command then resolves against the target — the same rule a `cd` would give.
27
+ const vaultAt = process.argv.indexOf('--vault', 2);
28
+ if (vaultAt !== -1) {
29
+ const target = process.argv[vaultAt + 1];
30
+ if (!target || target.startsWith('-')) {
31
+ console.error('\u2716 --vault needs a path to a workspace, e.g. `--vault ../another-workspace`');
32
+ process.exit(1);
33
+ }
34
+ const resolved = path.resolve(process.cwd(), target);
35
+ if (!fs.existsSync(resolved)) {
36
+ console.error(`\u2716 --vault ${target} \u2014 no such directory (resolved to ${resolved})`);
37
+ process.exit(1);
38
+ }
39
+ process.chdir(resolved);
40
+ process.argv.splice(vaultAt, 2);
41
+ }
42
+
15
43
  const devBin = findDevClone(process.cwd());
16
44
  // `realpath`, NOT `path.resolve`: resolve is pure string math and does not follow symlinks, so a
17
45
  // workspace whose git_modules/dreamteamer is a SYMLINK to the engine it is already running (the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.25.1",
3
+ "version": "0.26.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>",
@@ -71,6 +71,22 @@ on: **validation is hard** (unknown fields included; an invalid write is rejecte
71
71
  with no partial state), and **a write does not commit** — `dt commit` publishes, scoped
72
72
  (`references/records.md`).
73
73
 
74
+ ## one session, several workspaces
75
+
76
+ a harness can be pointed at more than one workspace at a time. when it is, it loads skills, commands
77
+ and agents from ALL of them and the workspace INSTRUCTIONS from the PRIMARY one only — measured:
78
+ zero instruction blocks from the others across every session on record. so the schema in context is
79
+ the primary workspace's, whoever shipped the skill you are reading.
80
+
81
+ - **start a session in the workspace it is about.** it is the only thing that loads that workspace's
82
+ instructions, and it settles name collisions in its favour.
83
+ - **to touch another workspace from here, `--vault <path>` — not `cd`.** the flag resolves the
84
+ target's schema, validates and commits there, and leaves the working directory alone; a `cd`
85
+ outlives the command.
86
+ - **every record write prints the workspace it landed in** — `✔ <workspace> · <path>`. a write to
87
+ the wrong workspace passes validation, because the collection usually exists in both, so that line
88
+ is what distinguishes them.
89
+
74
90
  ## two acts, one map
75
91
 
76
92
  Act one is **working with data** — the records themselves. Act two is **modeling the workspace**
package/src/cli.js CHANGED
@@ -35,6 +35,10 @@ const QUIET = ['ignore', 'pipe', 'ignore'];
35
35
 
36
36
  export const USAGE = `usage: dreamteamer <verb> [<target>] [flags]
37
37
 
38
+ --vault <path> run against another workspace. The path resolves from the directory you typed it
39
+ in; every other path in the command resolves inside the target. The working
40
+ directory is unchanged.
41
+
38
42
  record verbs (hard validation — invalid writes are rejected before disk).
39
43
  A <target> is either a collection name or a <collection>/<id> reference; the reference splits at
40
44
  the longest DECLARED collection prefix, so finance/transactions/2026/03/coffee is ONE argument:
@@ -269,6 +273,16 @@ const FIELD_VERBS = ['add-field', 'set-field', 'rm-field', 'rename-field'];
269
273
  // misspelling — `dt commit --dryrun` COMMITTED, because `rest.includes('--dry-run')` is false for a
270
274
  // flag nobody typed correctly. The record/system/field verbs are checked in `collections-cli.js`,
271
275
  // beside the parser they share; these nine have no shared parser, so the table is here.
276
+ // Flags consumed BEFORE dispatch, in bin/dreamteamer.js, so no verb ever sees them. They are real
277
+ // and honoured; they are simply honoured earlier than VERB_FLAGS/WORKSPACE_FLAGS can express.
278
+ //
279
+ // ⚠ Listing one here EXEMPTS it from the "documented flags are accepted by some verb" guard, which
280
+ // exists because 0.19.0 shipped a flag that was documented and silently swallowed. The exemption is
281
+ // only safe while the behaviour is pinned somewhere else: `--vault` is covered end-to-end in
282
+ // test/integration/cross-workspace-targeting.test.js. A global flag with no such file is the exact
283
+ // defect that guard was written to catch \u2014 add the coverage, not the entry.
284
+ export const GLOBAL_FLAGS = ['vault'];
285
+
272
286
  export const WORKSPACE_FLAGS = {
273
287
  init: ['name', 'data-path', 'harnesses', 'workspace-module'], update: [],
274
288
  install: ['clone', 'dry-run', 'json', 'link-env', 'all', 'hook', 'print-adapters'],
@@ -118,6 +118,14 @@ export function collectionCommand(ws, collection, verb, args) {
118
118
 
119
119
  const d = store.descriptor(collection);
120
120
 
121
+ // Every record write says WHICH workspace it landed in. `--vault` is OPTIONAL and a bare command
122
+ // resolves from the working directory, so the one thing the flag cannot do is tell you when you
123
+ // forgot it \u2014 this can, for the price of one word, with no state and nothing refused. A write
124
+ // aimed at one workspace and landed in another becomes visible on the very next line of output
125
+ // instead of never: on a machine holding several workspaces most collection NAMES are shared while
126
+ // their fields are not, so the misplaced write succeeds and reports success.
127
+ const at = ws.pkg?.name ? `${ws.pkg.name} \u00b7 ` : '';
128
+
121
129
  switch (verb) {
122
130
  case 'list': {
123
131
  const { rows, narrowed } = narrowRows(store, d, collection, flags);
@@ -167,7 +175,7 @@ export function collectionCommand(ws, collection, verb, args) {
167
175
  const id = need(pos, 0, 'id');
168
176
  if (!flags.from) throw new Error(`"${collection}" is a \`codec: file\` collection — pass --from <path> with the file to import`);
169
177
  const { id: written, file } = store.addFile(collection, id, flags.from, { force: !!flags.force });
170
- flags.json ? emit(JSON.stringify({ id: written, path: rel(ws.root, file) })) : console.log(`✔ ${rel(ws.root, file)}`);
178
+ flags.json ? emit(JSON.stringify({ id: written, path: rel(ws.root, file) })) : console.log(`✔ ${at}${rel(ws.root, file)}`);
171
179
  return 0;
172
180
  }
173
181
  if (flags.from) throw new Error(`--from imports a file as a record, and "${collection}" is not a \`codec: file\` collection`);
@@ -175,7 +183,7 @@ export function collectionCommand(ws, collection, verb, args) {
175
183
  const { id, file, idFallback } = store.add(collection, fields, { id: flags.id });
176
184
  flags.json
177
185
  ? emit(JSON.stringify({ id, path: rel(ws.root, file), ...(idFallback ? { idFallback } : {}) }))
178
- : console.log(`✔ ${rel(ws.root, file)}`);
186
+ : console.log(`✔ ${at}${rel(ws.root, file)}`);
179
187
  // ⚠ SAY IT, EVERY TIME. The id was derived from a hash because the value it is generated
180
188
  // from carries no a-z0-9 — the write succeeded and the record is fine, but the id is
181
189
  // unreadable and unguessable, and nobody finds that out until they try to type it. By
@@ -194,7 +202,7 @@ export function collectionCommand(ws, collection, verb, args) {
194
202
  Object.assign(changes, coerceArrays(d, stripMeta(flags)));
195
203
  if (!Object.keys(changes).length) throw new Error('nothing to set — pass key=value pairs or --key value flags');
196
204
  store.set(collection, id, changes);
197
- flags.json ? emit(JSON.stringify({ id })) : console.log('✔ updated');
205
+ flags.json ? emit(JSON.stringify({ id })) : console.log(`✔ ${at}updated`);
198
206
  return 0;
199
207
  }
200
208
  // Manual ordering. ONE record is written per move — that is the entire feature; a dense
@@ -228,20 +236,20 @@ export function collectionCommand(ws, collection, verb, args) {
228
236
 
229
237
  const id = need(pos, 0, 'id');
230
238
  store.set(collection, id, { [field]: placementKey(rows, id, flags, collection) });
231
- flags.json ? emit(JSON.stringify({ id })) : console.log('✔ moved');
239
+ flags.json ? emit(JSON.stringify({ id })) : console.log(`✔ ${at}moved`);
232
240
  return 0;
233
241
  }
234
242
  case 'rm': {
235
243
  const id = need(pos, 0, 'id');
236
244
  if (flags['dry-run']) return dryRunPlan(`rm ${collection}/${id}`, { records: store.ids(collection).has(id) ? 1 : 0, refs: store.findInboundRefs(`${collection}/${id}`).length });
237
245
  const { inboundIgnored } = store.rm(collection, id, { force: !!flags.force });
238
- flags.json ? emit(JSON.stringify({ id, removed: true, inboundIgnored })) : console.log(`✔ removed${inboundIgnored ? ` (${inboundIgnored} inbound reference(s) left dangling — run \`dreamteamer check\`)` : ''}`);
246
+ flags.json ? emit(JSON.stringify({ id, removed: true, inboundIgnored })) : console.log(`✔ ${at}removed${inboundIgnored ? ` (${inboundIgnored} inbound reference(s) left dangling — run \`dreamteamer check\`)` : ''}`);
239
247
  return 0;
240
248
  }
241
249
  case 'rename': {
242
250
  const out = store.rename(collection, need(pos, 0, 'old id'), need(pos, 1, 'new id'));
243
251
  if (flags.json) { emit(JSON.stringify(out)); return 0; }
244
- console.log(`✔ renamed ${collection}/${need(pos, 0, 'old id')} → ${collection}/${out.id}`);
252
+ console.log(`✔ ${at}renamed ${collection}/${need(pos, 0, 'old id')} → ${collection}/${out.id}`);
245
253
  if (out.touched) console.log(`✔ rewrote ${out.rewrites} inbound reference(s) across ${out.touched} file(s)`);
246
254
  return 0;
247
255
  }