dreamteamer 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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>",
package/src/cli.js CHANGED
@@ -12,6 +12,12 @@ import { deriveEvents } from './events.js';
12
12
  import { commitPending } from './commit.js';
13
13
  import { Store } from './store.js';
14
14
 
15
+ // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
16
+ // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
17
+ // reached the user's terminal. stdout stays piped because we read it.
18
+ const QUIET = ['ignore', 'pipe', 'ignore'];
19
+
20
+
15
21
  const USAGE = `usage: dreamteamer <command> | dreamteamer <collection> <verb> …
16
22
 
17
23
  commands:
@@ -230,7 +236,7 @@ export function run(argv) {
230
236
  }
231
237
 
232
238
  function tryGit(cwd, args) {
233
- try { return execFileSync('git', args, { cwd }).toString().trim() || null; } catch { return null; }
239
+ try { return execFileSync('git', args, { cwd, stdio: QUIET }).toString().trim() || null; } catch { return null; }
234
240
  }
235
241
 
236
242
  function watchAndRecompile(ws) {
package/src/commit.js CHANGED
@@ -6,6 +6,12 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { pathToRecord } from './events.js';
8
8
 
9
+ // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
10
+ // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
11
+ // reached the user's terminal. stdout stays piped because we read it.
12
+ const QUIET = ['ignore', 'pipe', 'ignore'];
13
+
14
+
9
15
  const VERB = { A: 'add', M: 'set', D: 'rm', R: 'rename', '?': 'add' };
10
16
 
11
17
  /** Record directories to watch, grouped by owning repo. System-stored collections are excluded:
@@ -31,7 +37,7 @@ function scopeByRepo(descriptors, only) {
31
37
  * is NOT a refusal condition — committing dirty records is this verb's entire job. */
32
38
  function inProgress(cwd) {
33
39
  let gitDir;
34
- try { gitDir = execFileSync('git', ['rev-parse', '--absolute-git-dir'], { cwd }).toString().trim(); }
40
+ try { gitDir = execFileSync('git', ['rev-parse', '--absolute-git-dir'], { cwd, stdio: QUIET }).toString().trim(); }
35
41
  catch { return 'not a git repository'; }
36
42
  for (const [marker, label] of [['MERGE_HEAD', 'merge'], ['rebase-merge', 'rebase'], ['rebase-apply', 'rebase'], ['CHERRY_PICK_HEAD', 'cherry-pick'], ['REVERT_HEAD', 'revert']]) {
37
43
  if (fs.existsSync(path.join(gitDir, marker))) return `a ${label} is in progress`;
@@ -42,7 +48,7 @@ function inProgress(cwd) {
42
48
  /** A commit on a detached HEAD is reachable only by sha. Worth saying out loud before making one;
43
49
  * not worth refusing over, since it is sometimes exactly what someone means to do. */
44
50
  function detached(cwd) {
45
- try { return execFileSync('git', ['symbolic-ref', '--quiet', 'HEAD'], { cwd }).toString().trim() === ''; }
51
+ try { return execFileSync('git', ['symbolic-ref', '--quiet', 'HEAD'], { cwd, stdio: QUIET }).toString().trim() === ''; }
46
52
  catch { return true; }
47
53
  }
48
54
 
@@ -57,7 +63,10 @@ function sample(root, repo, dirs, descriptors) {
57
63
  // `-uall` is load-bearing: by default porcelain COLLAPSES an untracked directory to a single
58
64
  // `?? data/notes/` entry, which maps to no record — so the first records of a brand-new
59
65
  // collection would be invisible and dt commit would report success having committed nothing.
60
- const out = execFileSync('git', ['status', '--porcelain', '-z', '-uall', '--', ...relDirs], { cwd }).toString();
66
+ // QUIET: the caller (cli `status`) catches a failure here so it can still print the rest of the
67
+ // report in a non-git folder — but git's own "fatal: not a git repository" was reaching the
68
+ // terminal anyway, which made a handled case look like a crash.
69
+ const out = execFileSync('git', ['status', '--porcelain', '-z', '-uall', '--', ...relDirs], { cwd, stdio: QUIET }).toString();
61
70
  const chunks = out.split('\0').filter((c) => c.length > 0);
62
71
  const rows = [];
63
72
  for (let i = 0; i < chunks.length; i++) {
package/src/compile.js CHANGED
@@ -100,7 +100,8 @@ function bothLayouts(root, kind) {
100
100
  * has `data/` — not a layout knob every module would set identically.
101
101
  */
102
102
  const NON_SOURCE_DIRS = new Set([
103
- 'node_modules', 'data', 'state', 'media', 'bin', 'src', 'lib', 'scripts', 'studio',
103
+ 'node_modules', 'data', 'state', 'media', 'bin', 'src', 'lib', 'scripts',
104
+ 'ui', 'studio', // the module's UI bundle — 'studio' is the pre-archive name, kept as a fallback
104
105
  'docs', 'dist', 'build', 'test', 'tests', 'coverage', 'system', // 'system': the pre-flatten layout
105
106
  ]);
106
107
 
@@ -357,23 +358,21 @@ export function compile({ root, pkg }) {
357
358
  }
358
359
  }
359
360
 
360
- // A module that ships only folders the engine does not recognise compiles ✔ and contributes
361
- // NOTHING. Warn; do not fail, since a module that is temporarily source-free is the
362
- // operator's business, not the compiler's.
363
- for (const source of sources) {
364
- if (contributed.has(source.name)) continue;
365
- console.warn(`⚠ module "${source.name}" (${rel(source.root)}) contributed no recognised sources — its folder names must match a known kind (${KINDS.join(', ')})`);
366
- }
367
-
368
361
  // ---- stage module UI bundles ---------------------------------------------------
369
- // modules ship a PRE-BUILT app.js that registers components/layouts against the studio
362
+ // modules ship a PRE-BUILT app.js that registers components/layouts against the surface's
370
363
  // registry (design "the UI": components are module code, never records). staged under
371
- // .dreamteamer/ui/<module>/app.js; the server serves /ui, the studio imports and calls it.
372
- // studio/dist/app.js (a built bundle) wins over studio/app.js (plain-JS, host-provided Vue).
364
+ // .dreamteamer/ui/<module>/app.js; the VS Code extension reads it off disk (decision 48) and
365
+ // the legacy server served it at /ui. `dist/app.js` (a built bundle) wins over `app.js`
366
+ // (plain-JS, host-provided Vue).
367
+ //
368
+ // `ui/` is the name — it matches where the bundle STAGES and what it is. `studio/` is the
369
+ // original name and stays a fallback: the studio it referred to is archived (decisions 51, 93),
370
+ // so the folder was named after a surface that no longer exists. Both are in NON_SOURCE_DIRS,
371
+ // so neither trips the unknown-folder gate (decision 179).
373
372
  const uiModules = [];
374
373
  const uiOwners = new Map(); // shortName -> module name, for a readable collision error
375
374
  for (const source of sources) {
376
- const cand = ['studio/dist/app.js', 'studio/app.js']
375
+ const cand = ['ui/dist/app.js', 'ui/app.js', 'studio/dist/app.js', 'studio/app.js']
377
376
  .map((p) => path.join(source.root, p))
378
377
  .find((p) => fs.existsSync(p));
379
378
  if (!cand) continue;
@@ -386,6 +385,18 @@ export function compile({ root, pkg }) {
386
385
  uiOwners.set(shortName, source.name);
387
386
  addEntry(path.join('ui', shortName, 'app.js'), cand);
388
387
  uiModules.push(shortName);
388
+ // A UI bundle IS a contribution. Counting it here is what keeps the warning below honest —
389
+ // a module whose whole purpose is a layout used to be told it "contributed no recognised
390
+ // sources" while its layout was rendering in the app.
391
+ contributed.add(source.name);
392
+ }
393
+
394
+ // A module that ships only folders the engine does not recognise compiles ✔ and contributes
395
+ // NOTHING. Warn; do not fail, since a module that is temporarily source-free is the
396
+ // operator's business, not the compiler's. Runs AFTER UI staging so a UI-only module counts.
397
+ for (const source of sources) {
398
+ if (contributed.has(source.name)) continue;
399
+ console.warn(`⚠ module "${source.name}" (${rel(source.root)}) contributed no recognised sources — its folder names must match a known kind (${KINDS.join(', ')}) or it must ship a UI bundle at ui/app.js`);
389
400
  }
390
401
 
391
402
  // ---- collection-templates, for `templates:` merging ----------------------------
@@ -512,29 +523,27 @@ export function compile({ root, pkg }) {
512
523
  }
513
524
 
514
525
  // ---- ui-view layout validation --------------------------------------------------
515
- // layouts are registered module code; a view naming an unregistered layout fails loudly
516
- // naming the registered set (design guardrail: "unknown layout = compile error").
517
- // core set = the studio's built-ins; modules declare theirs in package.json
518
- // dreamteamer.studio.layouts (the same file their app.js registration lives beside).
519
- // KEEP IN SYNC with the UI's `lists.register(...)` calls (dreamteamer-vscode
520
- // webview/src/registry/register-defaults.ts). kanban/calendar/map landed there as core Lists in
521
- // the 2026-07-27 layouts wave but this set was never widened, so the only way to get a
522
- // `layout: kanban` view past compile was for a module to CLAIM the layout it didn't own — which
523
- // is what a workspace module was once caught doing, shadowing the core board in the registry (a
524
- // module's app.js loads after the built-ins and Map.set wins). Fixed both ends 2026-07-29.
525
- const registeredLayouts = new Set(['table', 'cards', 'kanban', 'calendar', 'map']);
526
- for (const source of sources) {
527
- try {
528
- const mpkg = JSON.parse(fs.readFileSync(path.join(source.root, 'package.json'), 'utf8'));
529
- for (const l of mpkg.dreamteamer?.studio?.layouts ?? []) registeredLayouts.add(l);
530
- } catch { /* root-workspace source without package.json */ }
531
- }
526
+ // `layout` is NOT validated here, deliberately. The rule: the engine validates a value if and
527
+ // only if the ENGINE INTERPRETS it. It interprets filter operators (`matchesFilter`, and the
528
+ // CLI's `--where`), so a typo'd operator is a real bug it can catch — hence the check below.
529
+ // It interprets `layout` nowhere: the value is opaque payload forwarded to whichever surface
530
+ // renders, and only that surface's registry knows which ids exist.
531
+ //
532
+ // There used to be an allowlist here, hardcoded to mirror dreamteamer-vscode's
533
+ // `lists.register(...)` calls in a DIFFERENT REPO. It was wrong both times it was tested:
534
+ // kanban/calendar/map (2026-07-29) and erd/graph (2026-08-10), each costing an engine edit to
535
+ // add a UI feature. Worse, it BLOCKED the sanctioned extension path a module's `app.js` gets
536
+ // a `registerList({ id, ... })` API, so it can contribute a layout with no engine involvement,
537
+ // and this check then rejected the very view naming it unless the module also duplicated the id
538
+ // into a `dreamteamer.studio.layouts` key (zero users, in any repo, ever). Proven 2026-08-11 by
539
+ // modules/ui-smoke: the layout rendered in the app while compile refused the view.
540
+ //
541
+ // The descriptor already documented the correct behaviour — ui-views.collection.yaml: "An
542
+ // unregistered id degrades visibly rather than erroring" — and the surface already implements
543
+ // it (presets.ts#resolveRendererEntry falls back to table). Decision 195.
532
544
  for (const [rt, e] of entries) {
533
545
  if (!rt.startsWith('ui-views/')) continue;
534
546
  const view = load(e.bytes.toString('utf8'));
535
- if (view?.target === 'list' && view?.layout && !registeredLayouts.has(view.layout)) {
536
- fail(`${rt}: layout "${view.layout}" is not registered (registered: ${[...registeredLayouts].sort().join(', ')}).\n a module registers layouts in its studio app.js AND declares them in package.json under dreamteamer.studio.layouts.`);
537
- }
538
547
  // filters are load-bearing (they narrow what the operator SEES) — typo'd operators
539
548
  // fail at compile, not silently at render (review finding 5)
540
549
  const badOps = view?.filter ? [...unknownOperators(view.filter)] : [];
package/src/init.js CHANGED
@@ -9,6 +9,12 @@ import { slugOrHash } from './template.js';
9
9
  import { discoverModules, KINDS } from './compile.js';
10
10
  import { Store } from './store.js';
11
11
 
12
+ // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
13
+ // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
14
+ // reached the user's terminal. stdout stays piped because we read it.
15
+ const QUIET = ['ignore', 'pipe', 'ignore'];
16
+
17
+
12
18
  const SKELETON_KINDS = ['collections', 'skills', 'agents', 'commands', 'ui-views'];
13
19
 
14
20
  const GITIGNORE = `node_modules/
@@ -137,6 +143,7 @@ export function install({ root, pkg }) {
137
143
  const names = Object.keys(map);
138
144
  if (!names.length) { console.log('✔ no git-modules declared — nothing to restore'); return 0; }
139
145
  fs.mkdirSync(path.join(root, 'git_modules'), { recursive: true });
146
+ const unreachable = [];
140
147
  for (const name of names) {
141
148
  const { url, ref = 'main' } = map[name];
142
149
  const dest = path.join(root, 'git_modules', name);
@@ -148,10 +155,24 @@ export function install({ root, pkg }) {
148
155
  continue;
149
156
  }
150
157
  console.log(`… cloning ${url} → git_modules/${name} (${ref})`);
151
- execFileSync('git', ['clone', '--branch', ref, url, dest], { stdio: 'inherit' });
158
+ try {
159
+ execFileSync('git', ['clone', '--branch', ref, url, dest], { stdio: 'inherit' });
160
+ } catch {
161
+ // one unreachable clone must not abandon the rest. the lockfile is a map of PRIVATE
162
+ // repos as often as public ones, so "the current credentials cannot see this one" is
163
+ // ordinary — a fresh machine, a collaborator, a cloud sandbox. aborting there left a
164
+ // half-restored workspace and named only the first failure.
165
+ fs.rmSync(dest, { recursive: true, force: true }); // git leaves the partial dir behind
166
+ unreachable.push(name);
167
+ console.warn(`⚠ git_modules/${name}: clone failed — skipped (${url})`);
168
+ continue;
169
+ }
152
170
  buildClone(dest, name);
153
171
  }
154
- return 0;
172
+ // non-zero, because the workspace is NOT what the lockfile describes: modules are missing and
173
+ // `check` will report references into them as unknown collections.
174
+ if (unreachable.length) console.error(`✖ ${unreachable.length} module(s) could not be cloned: ${unreachable.join(', ')}`);
175
+ return unreachable.length ? 1 : 0;
155
176
  }
156
177
 
157
178
  // dreamteamer update [<name>] — pull each lockfile-declared git_modules clone forward
@@ -200,7 +221,7 @@ function buildClone(dest, name) {
200
221
  }
201
222
 
202
223
  function tryGit(cwd, args) {
203
- try { return execFileSync('git', args, { cwd }).toString().trim() || null; } catch { return null; }
224
+ try { return execFileSync('git', args, { cwd, stdio: QUIET }).toString().trim() || null; } catch { return null; }
204
225
  }
205
226
 
206
227
  function appendMissing(file, block) {
package/src/server.js CHANGED
@@ -16,6 +16,12 @@ import { commandsFor, recordResolver } from './record-commands.js';
16
16
  import { distinctValues } from './field-values.js';
17
17
  import { slugOrHash } from './template.js';
18
18
 
19
+ // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
20
+ // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
21
+ // reached the user's terminal. stdout stays piped because we read it.
22
+ const QUIET = ['ignore', 'pipe', 'ignore'];
23
+
24
+
19
25
  export function startServer(ws, { port = 8080, host = '127.0.0.1' } = {}) {
20
26
  const app = express();
21
27
  app.use(express.json({ limit: '10mb' }));
@@ -42,7 +48,7 @@ export function startServer(ws, { port = 8080, host = '127.0.0.1' } = {}) {
42
48
  // so `@me` filters in ui-views resolve to the seeded user record.
43
49
  let operatorId = null;
44
50
  try {
45
- operatorId = slugOrHash(execFileSync('git', ['config', 'user.name'], { cwd: ws.root }).toString().trim());
51
+ operatorId = slugOrHash(execFileSync('git', ['config', 'user.name'], { cwd: ws.root, stdio: QUIET }).toString().trim());
46
52
  } catch { /* no git identity — @me filters simply won't narrow */ }
47
53
 
48
54
  api.get('/info', (req, res) => {
package/src/store.js CHANGED
@@ -12,6 +12,12 @@ import { generateId } from './template.js';
12
12
  import { parseRecord, parseRecordText, patternRe, fmtAjvError, unknownFields, walk, EXT, assertSafeId } from './records.js';
13
13
  import { normalizeRecord } from './temporal.js';
14
14
  import { NO_RUNTIME, loadDescriptors, runtimeDir, sourceRoots as compiledSourceRoots } from './runtime.js';
15
+
16
+ // git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
17
+ // child's stderr to ours unless told otherwise, so a handled "not a git repository" still
18
+ // reached the user's terminal. stdout stays piped because we read it.
19
+ const QUIET = ['ignore', 'pipe', 'ignore'];
20
+
15
21
  export class Store {
16
22
  constructor({ root, pkg }) {
17
23
  this.root = root;
@@ -66,7 +72,7 @@ export class Store {
66
72
 
67
73
  // current HEAD — one cheap rev-parse per cache check vs a multi-thousand-file walk
68
74
  gitHead() {
69
- try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: this.root }).toString().trim(); } catch { return 'no-git'; }
75
+ try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: this.root, stdio: QUIET }).toString().trim(); } catch { return 'no-git'; }
70
76
  }
71
77
 
72
78
  ids(collection) {
@@ -128,7 +134,7 @@ export class Store {
128
134
  const relPath = path.relative(this.root, file);
129
135
  let previousContent;
130
136
  try {
131
- previousContent = execFileSync('git', ['show', `${hash}:${relPath}`], { cwd: this.root }).toString();
137
+ previousContent = execFileSync('git', ['show', `${hash}:${relPath}`], { cwd: this.root, stdio: QUIET }).toString();
132
138
  } catch {
133
139
  throw new Error(`${collection}/${id}: no content at ${hash} for ${relPath} — nothing was reverted.`);
134
140
  }