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
@@ -0,0 +1,135 @@
1
+ // Engine extensions — the ONE seam an optional tool plugs into.
2
+ //
3
+ // Core is records + the workspace compiler, and nothing that has a lifecycle of its own: an HTTP
4
+ // server, a Docker host, a behaviour-test runner, an exporter to one vendor. Those ship as sibling
5
+ // packages, and a workspace opts into one by DEPENDING on it — a direct dependency whose package.json
6
+ // declares `"dreamteamer": { "extension": "./entry.js" }`. That is the whole declaration: npm already
7
+ // put the code there on purpose, and a transitive package is never loaded however it advertises.
8
+ //
9
+ // The entry's default export is `activate(dt)` — `dt` is the RUNNING engine's public API (api.js), so
10
+ // an extension never imports a second engine copy of its own and can never disagree with the one the
11
+ // operator ran (the dev-clone shadow and `--vault` both pick the engine before this file loads).
12
+ // A workspace's own `modules/<id>/` may declare the same key: its code is the operator's, so it
13
+ // loads without a package — the way a workspace carries an extension nobody has published.
14
+ // It returns a contribution, every key optional:
15
+ //
16
+ // commands { <verb>: { usage, run(ws, argv) } } `dt <verb> …`, dispatched in-process
17
+ // sourceKinds [{ kind, exclude?: [subtree] }] folders compile stages like a built-in kind
18
+ // analyze (draft) → { errors?, warnings?, notes? } judged after assembly, before any output
19
+ // harnesses { <id>: (ctx) → { blocks: {file: text}, summary } } a harness adapter
20
+ // orientation string one paragraph appended to the orientation block
21
+ // hooks { <ClaudeHookEvent>: '<dt verb args>' } rendered by `dt install --print-adapters`
22
+ //
23
+ // Two contributions claiming the same verb, kind or harness is a refusal — there is no "last one
24
+ // wins", because the loser would be an extension the operator installed that silently does nothing.
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { pathToFileURL } from 'node:url';
28
+
29
+ export const EXTENSION_API = 1;
30
+
31
+ const CONTRIBUTION_KEYS = new Set(['commands', 'sourceKinds', 'analyze', 'harnesses', 'orientation', 'hooks']);
32
+
33
+ /** The modules that declare an extension entry: the workspace's own `modules/<id>/` first, then its
34
+ * direct dependencies, each sorted by name. A workspace module is the operator's own code, exactly
35
+ * like its `bin/`, so it needs no package and no npm — and it SHADOWS a dependency of the same name,
36
+ * the rule module content already follows. A module DISABLED by a bare `dreamteamer.disable` entry
37
+ * is not an extension either — disabling is how a workspace keeps a module and switches it off. */
38
+ export function declaredExtensions(ws) {
39
+ const disable = ws.pkg?.dreamteamer?.disable ?? [];
40
+ const out = [];
41
+ const seen = new Set();
42
+ const consider = (dir, fallback) => {
43
+ let pkg;
44
+ try { pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); } catch { return; }
45
+ const entry = pkg.dreamteamer?.extension;
46
+ const name = pkg.name ?? fallback;
47
+ if (!entry || seen.has(name) || disablesPackage(disable, name)) return;
48
+ if (typeof entry !== 'string') throw new Error(`${name}: dreamteamer.extension must be a path to the entry module (got ${JSON.stringify(entry)})`);
49
+ seen.add(name);
50
+ out.push({ name, version: pkg.version ?? '0.0.0', dir, entry: path.join(dir, entry) });
51
+ };
52
+ let inline = [];
53
+ try { inline = fs.readdirSync(path.join(ws.root, 'modules')).sort(); } catch { /* no modules/ */ }
54
+ for (const id of inline) consider(path.join(ws.root, 'modules', id), id);
55
+ for (const dep of Object.keys({ ...ws.pkg?.dependencies, ...ws.pkg?.devDependencies }).sort()) consider(path.join(ws.root, 'node_modules', dep), dep);
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Does a `dreamteamer.disable` list switch off the WHOLE package `name`? An entry names a package by
61
+ * its full name (`@scope/kit`, `probe-kit`) or by its module id — the name with the npm
62
+ * scope stripped (`kit`), which is what every engine message calls a module. Anything else with
63
+ * a slash is `<module>/<entity>`, one entity of a module, and never the package.
64
+ *
65
+ * ⚠ The scoped full name used to be read as `<module>/<entity>` because it contains a slash, and the
66
+ * bare id was compared against the full name — so neither spelling could disable a scoped package.
67
+ * Every extension this project ships is scoped.
68
+ */
69
+ export function disablesPackage(disable, name) {
70
+ const id = String(name).replace(/^@[^/]+\//, '');
71
+ return (disable ?? []).some((d) => typeof d === 'string' && (d === name || d === id));
72
+ }
73
+
74
+ /** Is a `dreamteamer.disable` entry a whole-package name rather than `<module>/<entity>`? A bare
75
+ * word, or a scoped npm name (`@scope/name` — one slash, leading `@`). */
76
+ export const isPackageEntry = (d) => typeof d === 'string' && (!d.includes('/') || /^@[^/]+\/[^/]+$/.test(d));
77
+
78
+ /**
79
+ * Import and activate every declared extension against `api`, and check the contributions cannot
80
+ * collide with core or with each other. `reserved` is what core already owns: its verbs, kinds and
81
+ * harness ids.
82
+ */
83
+ export async function loadExtensions(ws, api, reserved = {}) {
84
+ const loaded = [];
85
+ const owner = { command: new Map(), kind: new Map(), harness: new Map() };
86
+ for (const r of reserved.verbs ?? []) owner.command.set(r, 'the engine');
87
+ for (const r of reserved.kinds ?? []) owner.kind.set(r, 'the engine');
88
+ for (const r of reserved.harnesses ?? []) owner.harness.set(r, 'the engine');
89
+ const claim = (what, key, by) => {
90
+ const prev = owner[what].get(key);
91
+ if (prev) throw new Error(`extension ${by} contributes the ${what} "${key}", which ${prev} already owns — uninstall one, or switch it off: add "${by}" to dreamteamer.disable in package.json`);
92
+ owner[what].set(key, by);
93
+ };
94
+ for (const ext of declaredExtensions(ws)) {
95
+ let mod;
96
+ try { mod = await import(pathToFileURL(ext.entry).href); } catch (e) {
97
+ throw new Error(`extension ${ext.name}: its entry ${path.relative(ws.root, ext.entry)} did not load — ${e.message.split('\n')[0]} (npm install?)`);
98
+ }
99
+ if (typeof mod.default !== 'function') throw new Error(`extension ${ext.name}: ${path.relative(ws.root, ext.entry)} must default-export activate(dt)`);
100
+ if (mod.apiVersion !== undefined && mod.apiVersion !== EXTENSION_API) {
101
+ throw new Error(`extension ${ext.name} targets extension API ${mod.apiVersion}; this engine (${api.engineVersion?.() ?? '?'}) speaks ${EXTENSION_API}`);
102
+ }
103
+ const c = (await mod.default(api)) ?? {};
104
+ for (const k of Object.keys(c)) if (!CONTRIBUTION_KEYS.has(k)) throw new Error(`extension ${ext.name} contributes an unknown key "${k}" — known: ${[...CONTRIBUTION_KEYS].join(', ')}`);
105
+ const commands = c.commands ?? {};
106
+ for (const [verb, cmd] of Object.entries(commands)) {
107
+ if (typeof cmd?.run !== 'function') throw new Error(`extension ${ext.name}: command "${verb}" has no run(ws, argv)`);
108
+ claim('command', verb, ext.name);
109
+ }
110
+ const sourceKinds = (c.sourceKinds ?? []).map((k) => normalizeKind(ext.name, k));
111
+ for (const k of sourceKinds) claim('kind', k.kind, ext.name);
112
+ for (const id of Object.keys(c.harnesses ?? {})) claim('harness', id, ext.name);
113
+ if (c.analyze !== undefined && typeof c.analyze !== 'function') throw new Error(`extension ${ext.name}: analyze must be a function`);
114
+ loaded.push({ name: ext.name, version: ext.version, commands, sourceKinds, analyze: c.analyze ?? null, harnesses: c.harnesses ?? {}, orientation: c.orientation ?? null, hooks: c.hooks ?? {} });
115
+ }
116
+ return loaded;
117
+ }
118
+
119
+ /** A contributed source kind: a plain folder name, and excluded subtrees that stay RELATIVE to it. */
120
+ function normalizeKind(by, k) {
121
+ const kind = typeof k === 'string' ? k : k?.kind;
122
+ if (typeof kind !== 'string' || !/^[a-z][a-z0-9-]*$/.test(kind)) throw new Error(`extension ${by}: a source kind is a lowercase folder name (got ${JSON.stringify(kind)})`);
123
+ const exclude = (typeof k === 'object' ? k.exclude ?? [] : []).map((e) => {
124
+ const rel = path.posix.normalize(String(e)).replace(/\/+$/, '');
125
+ if (!rel || rel === '.' || rel.startsWith('..') || path.posix.isAbsolute(rel)) throw new Error(`extension ${by}: kind "${kind}" excludes "${e}", which is not a subtree of it`);
126
+ return rel;
127
+ });
128
+ return { kind, exclude, extension: by };
129
+ }
130
+
131
+ /** Is `relFromKind` (a '/'-separated path under the kind folder) inside one of its excluded subtrees? */
132
+ export function excludedFromKind(kinds, kind, relFromKind) {
133
+ const k = kinds.find((x) => x.kind === kind);
134
+ return !!k && k.exclude.some((e) => relFromKind === e || relFromKind.startsWith(`${e}/`));
135
+ }
package/src/filter.js CHANGED
@@ -99,7 +99,7 @@ export function unknownOperators(filter, found = new Set()) {
99
99
  return found;
100
100
  }
101
101
 
102
- // exported because `prove`'s enum validation has to compare the way THIS module compares: a proof's
102
+ // exported (through the public API) because a proof runner's enum validation has to compare the way THIS module compares: a proof's
103
103
  // `_eq: '5'` against a numeric enum member 5 is one filter at run time, and a strict `includes`
104
104
  // there refused a proof that works — worse than the silent pass it was written to prevent.
105
105
  export const looseEq = (v, o) => v === o || String(v) === String(o) || (typeof v === 'number' && Number(o) === v);
package/src/harnesses.js CHANGED
@@ -14,14 +14,14 @@ import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { load, dump } from './yaml.js';
16
16
 
17
- export const KNOWN_HARNESSES = ['claude-code', 'codex', 'pi', 'gemini-cli', 'cursor', 'notebooklm'];
17
+ export const KNOWN_HARNESSES = ['claude-code', 'codex', 'pi', 'gemini-cli', 'cursor'];
18
18
 
19
19
  export const STAMP = '<!-- generated by dreamteamer compile — do not edit; source of truth lives in modules/<module>/<kind>/ -->';
20
20
 
21
21
  // ⚠ THE VALUES ARE A CONTRACT, not an implementation detail: every managed root file on every disk
22
- // already carries these two exact strings, and `dt land` classifies a rebase conflict by asking
23
- // whether its hunks lie between them (src/land.js). Changing a byte orphans every block ever
24
- // written and silently reclassifies a generated conflict as the operator's own prose.
22
+ // already carries these two exact strings, and a tool that merges branches (a worktree
23
+ // extension's `land`) classifies a conflict by asking whether its hunks lie between them. Changing a byte orphans
24
+ // every block ever written and silently reclassifies a generated conflict as the operator's own prose.
25
25
  export const BEGIN = '<!-- dreamteamer:begin (generated — do not edit inside this block) -->';
26
26
  export const END = '<!-- dreamteamer:end -->';
27
27
 
@@ -31,7 +31,46 @@ export const END = '<!-- dreamteamer:end -->';
31
31
  export const INSTRUCTIONS_BEGIN = '<!-- dreamteamer:instructions:begin -->';
32
32
  export const INSTRUCTIONS_END = '<!-- dreamteamer:instructions:end -->';
33
33
 
34
- export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sourceLayout = 'flat', namespaces = [], version = 'unknown', workspaceModule = '' }) {
34
+ /** Both managed blocks as data, for a tool that has to recognise generated text in a user-owned file
35
+ * (a merge tool resolving a conflict) without importing marker strings from an internal module. The
36
+ * files carrying them are the manifest's `adapter-blocks`. */
37
+ export const MANAGED_BLOCKS = Object.freeze([
38
+ Object.freeze({ id: 'orientation', begin: BEGIN, end: END }),
39
+ Object.freeze({ id: 'instructions', begin: INSTRUCTIONS_BEGIN, end: INSTRUCTIONS_END }),
40
+ ]);
41
+
42
+ /**
43
+ * Run every piece of EXTENSION code the harness pass needs — contributed renderers and orientation
44
+ * paragraphs — and return plain data. Compile calls this BEFORE it replaces a single runtime byte: a
45
+ * renderer that throws used to do so halfway through materialization, leaving new descriptors beside
46
+ * the old manifest (review F7). Anything invalid is refused here with the extension's name.
47
+ */
48
+ export function renderContributions({ entries, harnesses, version = 'unknown', extensions = [] }) {
49
+ const rendered = [];
50
+ const paragraphs = [];
51
+ for (const e of extensions) {
52
+ const where = (what) => `extension ${e.name}: ${what}`;
53
+ try {
54
+ const p = typeof e.orientation === 'function' ? e.orientation({ entries }) : e.orientation;
55
+ if (typeof p === 'string' && p.trim()) paragraphs.push(p);
56
+ } catch (err) { throw new Error(where(`orientation failed — ${err.message}`)); }
57
+ for (const [id, render] of Object.entries(e.harnesses ?? {})) {
58
+ if (!harnesses.includes(id)) continue;
59
+ let out;
60
+ try { out = render({ entries, version, collections: buildCollectionsIndex(entries), modules: buildModulesIndex(entries) }) ?? {}; }
61
+ catch (err) { throw new Error(where(`harness "${id}" failed — ${err.message}`)); }
62
+ for (const [file, content] of Object.entries(out.blocks ?? {})) {
63
+ const norm = path.posix.normalize(file);
64
+ if (path.isAbsolute(file) || norm.startsWith('..') || norm.includes('/../')) throw new Error(where(`harness "${id}" writes "${file}", which is not a path inside the workspace`));
65
+ if (content != null && typeof content !== 'string') throw new Error(where(`harness "${id}" returned a ${typeof content} block for ${file} — a block is text, or null to remove it`));
66
+ }
67
+ rendered.push([id, out]);
68
+ }
69
+ }
70
+ return { rendered, paragraphs };
71
+ }
72
+
73
+ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sourceLayout = 'flat', namespaces = [], version = 'unknown', workspaceModule = '', extensions = [], kinds = [], contributions = renderContributions({ entries, harnesses, version, extensions }) }) {
35
74
  const outputs = [];
36
75
  // ⚠ SEPARATE from `outputs`: these are USER-OWNED root files carrying a managed block, and the
37
76
  // prune loop below DELETES anything in a previous manifest's `adapter-outputs` that this compile
@@ -49,11 +88,18 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
49
88
  outputs.push(out);
50
89
  };
51
90
 
91
+ // a harness an EXTENSION adapts (`harnesses: { <id>: render }`) is known exactly while it is installed
92
+ const contributed = new Map(extensions.flatMap((e) => Object.entries(e.harnesses ?? {})));
93
+ const known = [...KNOWN_HARNESSES, ...contributed.keys()];
52
94
  for (const h of harnesses) {
53
- if (!KNOWN_HARNESSES.includes(h)) console.warn(`⚠ unknown harness "${h}" in dreamteamer.harnesses — skipped (known: ${KNOWN_HARNESSES.join(', ')})`);
95
+ if (!known.includes(h)) console.warn(`⚠ unknown harness "${h}" in dreamteamer.harnesses — skipped (known: ${known.join(', ')})`);
54
96
  }
55
97
  const on = (h) => harnesses.includes(h);
56
98
  const skillsIndex = buildSkillsIndex(entries);
99
+ // what every orientation block carries beyond the schema: the source kinds extensions add, and the
100
+ // one paragraph each may contribute about itself
101
+ const extra = { kinds, paragraphs: contributions.paragraphs };
102
+ const orient = (flavor) => orientationBlock(flavor, skillsIndex, sourceLayout, namespaces, version, entries, workspaceModule, extra);
57
103
 
58
104
  // ---- claude-code: native skills/agents/commands dirs + CLAUDE.md block ----------
59
105
  if (on('claude-code')) {
@@ -73,7 +119,7 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
73
119
  }
74
120
  summary.push(`claude-code → .claude (${n} files)`);
75
121
  }
76
- block('CLAUDE.md', on('claude-code') ? orientationBlock('claude-code', skillsIndex, sourceLayout, namespaces, version, entries, workspaceModule) : null);
122
+ block('CLAUDE.md', on('claude-code') ? orient('claude-code') : null);
77
123
 
78
124
  // ---- shared cross-agent skills mirror (.agents/skills) — codex/pi discover it,
79
125
  // cursor/gemini blocks point at it. written once no matter how many harnesses use it.
@@ -90,12 +136,12 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
90
136
  }
91
137
 
92
138
  // ---- codex + pi: both read root AGENTS.md; one block serves both ----------------
93
- block('AGENTS.md', on('codex') || on('pi') ? orientationBlock('agents-md', skillsIndex, sourceLayout, namespaces, version, entries, workspaceModule) : null);
139
+ block('AGENTS.md', on('codex') || on('pi') ? orient('agents-md') : null);
94
140
  if (on('codex')) summary.push('codex → AGENTS.md block');
95
141
  if (on('pi')) summary.push('pi → AGENTS.md block + .agents/skills');
96
142
 
97
143
  // ---- gemini-cli: GEMINI.md is its context file -----------------------------------
98
- block('GEMINI.md', on('gemini-cli') ? orientationBlock('gemini', skillsIndex, sourceLayout, namespaces, version, entries, workspaceModule) : null);
144
+ block('GEMINI.md', on('gemini-cli') ? orient('gemini') : null);
99
145
  if (on('gemini-cli')) summary.push('gemini-cli → GEMINI.md block');
100
146
 
101
147
  // ---- the operator's own rules, one source, every harness -------------------------
@@ -125,18 +171,25 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
125
171
  // ⚠ `.mdc` is written WHOLE by `write()`, not through `writeBlock`, so removal is automatic: a
126
172
  // compile with no dreamteamer.md simply rewrites the file without the part.
127
173
  const instructionsPart = instructions ? `${INSTRUCTIONS_BEGIN}\n${instructions}\n${INSTRUCTIONS_END}\n\n` : '';
128
- const mdc = `---\ndescription: dreamteamer workspace orientation (generated)\nalwaysApply: true\n---\n\n${instructionsPart}${orientationBlock('cursor', skillsIndex, sourceLayout, namespaces, version, entries, workspaceModule)}\n\n${STAMP}\n`;
174
+ const mdc = `---\ndescription: dreamteamer workspace orientation (generated)\nalwaysApply: true\n---\n\n${instructionsPart}${orient('cursor')}\n\n${STAMP}\n`;
129
175
  write('.cursor/rules/dreamteamer.mdc', Buffer.from(mdc));
130
176
  summary.push('cursor → .cursor/rules/dreamteamer.mdc');
131
177
  }
132
178
 
133
- // ---- notebooklm: NOTEBOOKLM.md carries the notebook's CONFIGURATION ---------------
134
- // Not an orientation block: NotebookLM does not read a context file and has no skills. What it
135
- // needs is a persona to paste, a response length to pick, and the limits that decide how the
136
- // workspace has to be cut up — and all three go stale as collections come and go, which is why
137
- // this is generated on compile rather than written once by hand.
138
- block('NOTEBOOKLM.md', on('notebooklm') ? notebooklmBlock(entries, version) : null);
139
- if (on('notebooklm')) summary.push('notebooklm → NOTEBOOKLM.md block');
179
+ // ---- harnesses an extension adapts --------------------------------------------------
180
+ // A contributed adapter returns managed BLOCKS for user-owned files — never whole files, which
181
+ // would need an ownership story of their own. Read-only indexes are handed in so an adapter never
182
+ // re-parses the entries this module already parsed.
183
+ for (const [id, out] of contributions.rendered) {
184
+ for (const [file, content] of Object.entries(out.blocks ?? {})) block(file, content);
185
+ summary.push(out.summary ?? `${id} → ${Object.keys(out.blocks ?? {}).join(', ') || 'nothing'}`);
186
+ }
187
+ // A block this compile did NOT write, in a file a previous compile did, belongs to a harness that
188
+ // was switched off or an extension that was uninstalled: remove it, exactly as a built-in
189
+ // harness's own `block(file, null)` does. The file is the operator's; only the block goes.
190
+ for (const file of prevManifest?.['adapter-blocks'] ?? []) {
191
+ if (!blocks.includes(file) && !['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'].includes(file)) writeBlock(root, file, null);
192
+ }
140
193
 
141
194
  // ---- prune: anything WE stamped that this compile didn't produce ------------------
142
195
  const current = new Set(outputs);
@@ -158,103 +211,6 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
158
211
  return { outputs, blocks, summary };
159
212
  }
160
213
 
161
- /** Sources a notebook holds, per plan — Google's published table, read 2026-09. Stated here because
162
- * the number decides the SHAPE of the export: a workspace with more collections than the plan has
163
- * slots has to be narrowed or packed, and that is a decision the operator makes before running
164
- * anything. */
165
- const NOTEBOOK_PLANS = [
166
- ['standard', 50, 50], ['plus', 100, 200], ['pro', 300, 500], ['ultra', 600, 2500],
167
- ];
168
-
169
- /** The notebook's configuration, generated: what to paste as custom instructions, which settings to
170
- * pick, and the limits that bind. Derived from the compiled DESCRIPTORS only — never from `data/`,
171
- * for the same reason the orientation block carries no record counts: this lands in a committed file
172
- * and a count would re-dirty it on every write. */
173
- function notebooklmBlock(entries, version) {
174
- const index = buildCollectionsIndex(entries);
175
- const modules = buildModulesIndex(entries);
176
- // ⚠ `generated`, NOT `systemGroup` — the same split the orientation block's system line makes,
177
- // asked for the opposite reason. There the question is presentational ("group it out of the
178
- // domain listing"); HERE it is a STORAGE question, because every number and name below has to
179
- // describe what `dt export notebooklm` will actually ship, and the export ships by storage
180
- // (`storage.base !== 'runtime'` — export-notebooklm.js). Reading the partition here made this file
181
- // say "exports 1 collections" and list only `notes` for a workspace whose export shipped
182
- // `repos.md` as a second source and headed a `## module: System` group for it in the schema map
183
- // — so the persona generated from this line would not know about a source it had been given.
184
- // The count and the export are pinned to each other by a test (notebooklm-harness.test.js).
185
- const data = index.filter((c) => !c.generated);
186
- const withheldCollections = data.filter((c) => c.sensitive).map((c) => c.name);
187
- const withheldFields = data.flatMap((c) => c.sensitiveFields.map((f) => `${c.name}.${f}`));
188
- const exported = data.filter((c) => !c.sensitive);
189
-
190
- // grouped by module from the COLLECTIONS index, which carries each collection's owning module —
191
- // `buildModulesIndex` deliberately reports skills, commands and bin, not collections
192
- const title = new Map(modules.map((m) => [m.id, m.title]));
193
- const byModule = new Map();
194
- for (const c of data) {
195
- const k = c.module || '';
196
- if (!byModule.has(k)) byModule.set(k, []);
197
- byModule.get(k).push(c.name);
198
- }
199
- const brief = [...byModule]
200
- .sort((a, b) => (title.get(a[0]) || a[0]).localeCompare(title.get(b[0]) || b[0]))
201
- .map(([mod, names]) => `- ${title.get(mod) || mod || 'the workspace'}: ${names.sort().map((c) => (withheldCollections.includes(c) ? `${c} (withheld)` : c)).join(', ')}`);
202
-
203
- const persona = [
204
- 'You are the reference desk for this workspace. Every source is ONE COLLECTION of it, and every row or section is one record. Answer only from the sources.',
205
- '',
206
- 'A record is cited as `<collection>/<id>`. A field holding a reference contains exactly that form, so `leads.company = companies/acme` means the record `acme` in the `companies` source — follow those links across sources rather than guessing. A source whose name ends `--01`, `--02` is one collection split across files purely for size; treat the parts as one collection.',
207
- '',
208
- 'What this workspace keeps, by module:',
209
- ...brief,
210
- '',
211
- withheldCollections.length || withheldFields.length
212
- ? `Deliberately NOT here: ${[...withheldCollections.map((c) => `the whole collection ${c}`), ...withheldFields].join('; ')}. If asked about them, say they were withheld from the export rather than inferring.`
213
- : 'Nothing has been withheld from this export.',
214
- '',
215
- 'Ground every claim in a record and name it. When the question is a table question — which rows match, what is overdue, everything about one company — read down the column and give the matching records, not a summary. When the sources do not hold the answer, say so plainly: "this workspace does not record that" is a useful answer.',
216
- ].join('\n');
217
-
218
- const lines = [
219
- 'NOTEBOOKLM — the configuration for a notebook over this workspace. NotebookLM reads no context',
220
- 'file and runs no skills, so unlike the other harnesses nothing here is injected: it is applied,',
221
- `once per notebook, and re-pasted when the schema changes. Regenerated by every \`dreamteamer compile\` (engine ${version}).`,
222
- '',
223
- '## settings',
224
- '',
225
- '| setting | value | why |',
226
- '|---|---|---|',
227
- '| response length | **longer** | records are terse; the useful answer quotes several and says which |',
228
- '| chat mode | **do not set one** | `--mode` REPLACES the persona — see the warning below |',
229
- '',
230
- '```bash',
231
- 'notebooklm configure -n <notebook-id> --response-length longer \\',
232
- ' --persona "$(sed -n \'/^## custom instructions/,/^## limits/p\' NOTEBOOKLM.md | sed \'1,2d;$d\')"',
233
- '```',
234
- '',
235
- '⚠ **`configure` REPLACES the whole configuration on every call, and passing `--mode` with a persona silently discards the persona.** Measured: `--persona … --response-length longer --mode default` answers `{mode: "default", configured: true}` at exit 0, with the persona and the response length gone; the identical call without `--mode` returns both. There is also no read-only inspection — a bare `configure`, the obvious way to check the current settings, CLEARS them. So send every setting you want in ONE call, never `--mode` alongside a persona, and keep the persona in a file rather than only in the notebook.',
236
- '',
237
- '## custom instructions',
238
- '',
239
- persona,
240
- '',
241
- '## limits',
242
- '',
243
- '| plan | sources per notebook | chats per day |',
244
- '|---|---|---|',
245
- ...NOTEBOOK_PLANS.map(([n, s, c]) => `| ${n} | ${s} | ${c} |`),
246
- '',
247
- `This workspace exports **${exported.length} collections**${withheldCollections.length ? ` (${withheldCollections.length} withheld)` : ''}, so one source per collection needs a plan with at least that many slots — before any collection is split.`,
248
- '',
249
- '- **Per source:** 500,000 words or 200 MB, per Google. ⚠ A CSV source fails well below that: measured on a large private workspace, files at or under 805,081 bytes indexed and files at or above 881,828 bytes did not, so 700,000 bytes is the working ceiling.',
250
- '- **Persona:** 10,000 characters. The block above is generated to stay inside it; adding to it by hand can push it over, and `configure` refuses the whole call rather than truncating.',
251
- '- **Auto-sync follows native Google Docs, Sheets and Slides only.** An uploaded file — CSV, PDF, Markdown — is a SNAPSHOT: re-export means re-adding those sources.',
252
- '- ⚠ **`source add` reports success whether or not the file indexes.** Read `source list --json` back and require `type` to be the format you uploaded; a source that failed reads `type: unknown, status: error` with no message anywhere.',
253
- '- ⚠ **An answer is real only with non-blank text AND at least one reference.** Exit 0 with an empty answer is a real outcome, reached by scoping a broad question with `-s`.',
254
- ];
255
- return lines.join('\n');
256
- }
257
-
258
214
  // skill id → description one-liners from each SKILL.md's frontmatter; the orientation
259
215
  // block carries this index so harnesses without native skill discovery still get triggers.
260
216
  function buildSkillsIndex(entries) {
@@ -295,7 +251,6 @@ function buildCollectionsIndex(entries) {
295
251
  // the domain listing — the visible failure rather than the silent one.
296
252
  systemGroup: d.group === 'system',
297
253
  generated: d.storage?.base === 'runtime',
298
- driver: d.storage?.driver ?? null,
299
254
  description: flat(d.description),
300
255
  useWhen: flat(d.use_when),
301
256
  module: d.module ?? '',
@@ -336,18 +291,16 @@ function buildModulesIndex(entries) {
336
291
  let d = {};
337
292
  try { d = load(e.bytes.toString('utf8')) ?? {}; } catch { /* unparseable record */ }
338
293
  const p = !d.path || d.path === '.' ? '' : `${d.path}/`;
339
- mods.push({ id: m[1], title: d.title ?? m[1], description: flat(d.description), namespaces: d.namespaces ?? [], path: p, bin: d.bin ?? [], skills: [], commands: [], proofs: [] });
294
+ mods.push({ id: m[1], title: d.title ?? m[1], description: flat(d.description), namespaces: d.namespaces ?? [], path: p, bin: d.bin ?? [], skills: [], commands: [] });
340
295
  }
341
296
  mods.sort((a, b) => b.path.length - a.path.length); // longest prefix first
342
297
  for (const [rt, e] of entries) {
343
- const kind = /^skills\/([^/]+)\/SKILL\.md$/.exec(rt) ? 'skills' : /^commands\/(.+)\.command\.md$/.exec(rt) ? 'commands' : /^proofs\/(.+)\.proof\.yaml$/.exec(rt) ? 'proofs' : null;
298
+ const kind = /^skills\/([^/]+)\/SKILL\.md$/.exec(rt) ? 'skills' : /^commands\/(.+)\.command\.md$/.exec(rt) ? 'commands' : null;
344
299
  if (!kind) continue;
345
300
  const src = e.sources?.[0]?.path ?? '';
346
301
  const owner = mods.find((mod) => src.startsWith(mod.path));
347
- if (owner) owner[kind].push(kind === 'skills' ? rt.split('/')[1] : path.basename(rt).replace(/\.(command\.md|proof\.yaml)$/, ''));
302
+ if (owner) owner[kind].push(kind === 'skills' ? rt.split('/')[1] : path.basename(rt).replace(/\.command\.md$/, ''));
348
303
  }
349
- // `proofs` is COUNTED, never listed (see the module filter below), so it is deliberately not
350
- // sorted — an order nothing reads is work that reads as a promise the output does not keep.
351
304
  for (const mod of mods) { mod.skills.sort(); mod.commands.sort(); }
352
305
  return mods;
353
306
  }
@@ -404,7 +357,7 @@ function collectionsSection(index, modules, workspaceModule) {
404
357
  const data = index.filter((c) => !c.systemGroup);
405
358
  const isWs = (m) => m.path === `modules/${workspaceModule}/`;
406
359
  const groups = modules
407
- .filter((m) => { const own = index.filter((c) => c.module === m.id); return own.some((c) => !c.systemGroup) || (!own.length && (m.skills.length || m.commands.length || m.bin.length || m.proofs.length)); })
360
+ .filter((m) => { const own = index.filter((c) => c.module === m.id); return own.some((c) => !c.systemGroup) || (!own.length && (m.skills.length || m.commands.length || m.bin.length)); })
408
361
  .sort((a, b) => (isWs(b) - isWs(a)) || a.title.localeCompare(b.title));
409
362
  for (const m of groups) {
410
363
  const where = [`\`${m.id}\``, m.path ? m.path.replace(/\/$/, '') : 'the workspace root', ...(m.namespaces.length ? [`namespaces: ${m.namespaces.join(' · ')}`] : [])];
@@ -419,10 +372,7 @@ function collectionsSection(index, modules, workspaceModule) {
419
372
  }
420
373
  const sys = index.filter((c) => c.systemGroup);
421
374
  const system = sys.filter((c) => c.generated).map((c) => c.name);
422
- const kept = sys.filter((c) => !c.generated && !c.driver).map((c) => c.name);
423
- // A DRIVER collection is neither build output nor files: its verbs are answered by a driver over
424
- // something that runs (Docker), so it gets its own clause rather than being called either.
425
- const driven = sys.filter((c) => c.driver).map((c) => `${c.name} (${c.driver})`);
375
+ const kept = sys.filter((c) => !c.generated).map((c) => c.name);
426
376
  // ⚠ THIS LINE IS THE FIRST THING A SESSION READS about the system collections, and until 0.19.0
427
377
  // it said "schema-ops only", which named an internal module and a grammar that no longer exists.
428
378
  // It now names the VERBS and the one policy difference, because an agent that knows the verbs
@@ -432,7 +382,7 @@ function collectionsSection(index, modules, workspaceModule) {
432
382
  // told "it is build output" — a sentence that was false of it and is false of the next data-backed
433
383
  // system collection too, since the split is derived rather than naming one.
434
384
  if (sys.length) {
435
- lines.push('', `- system collections — the SAME verbs (add · set · rm · rename · list · get), plus \`dt add-field\`/\`set-field\`/\`rm-field\`/\`rename-field <collection>\`. A system write COMMITS ITSELF, in the repo holding the source; a record write does not (\`dt commit\` publishes).${system.length ? ` Never hand-edit \`.dreamteamer/\` — it is build output: ${system.join(' · ')}.` : ''}${kept.length ? ` Machinery whose records are real files you edit like any other: ${kept.join(' · ')}.` : ''}${driven.length ? ` Answered by a DRIVER, not files — nothing under data/, nothing to commit, the same verbs plus start · stop · open: ${driven.join(' · ')}` : ''}`);
385
+ lines.push('', `- system collections — the SAME verbs (add · set · rm · rename · list · get), plus \`dt add-field\`/\`set-field\`/\`rm-field\`/\`rename-field <collection>\`. A system write COMMITS ITSELF, in the repo holding the source; a record write does not (\`dt commit\` publishes).${system.length ? ` Never hand-edit \`.dreamteamer/\` — it is build output: ${system.join(' · ')}.` : ''}${kept.length ? ` Machinery whose records are real files you edit like any other: ${kept.join(' · ')}.` : ''}`);
436
386
  }
437
387
  return lines;
438
388
  }
@@ -523,7 +473,7 @@ function bindingsSection(entries) {
523
473
  * agent that splits `health/doctors/dana-levi` at the first slash reads a collection that does not
524
474
  * exist. Naming the declared list is what makes the grammar decidable from this block alone, without
525
475
  * the agent having to go read the manifest. A workspace with no namespaces gets no extra sentence. */
526
- function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces = [], version = 'unknown', entries = new Map(), workspaceModule = '') {
476
+ function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces = [], version = 'unknown', entries = new Map(), workspaceModule = '', extra = { kinds: [], paragraphs: [] }) {
527
477
  const sourcesLine = {
528
478
  flat: '`modules/<module>/<kind>/` — `collections/`, `skills/`, `agents/`, `commands/`,',
529
479
  nested: '`modules/<module>/system/<kind>/` — `collections/`, `skills/`, `agents/`, `commands/`,',
@@ -538,17 +488,14 @@ function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces
538
488
  'nouns. **read the `using-dreamteamer` skill before working with data or changing what the',
539
489
  'workspace keeps or does.** schemas (read): `.dreamteamer/collections/` (provenance:',
540
490
  '`.dreamteamer/manifest.yaml`). sources (write): ' + sourcesLine,
541
- '`command-bindings/`, `ui-views/`, `collection-templates/`, `proofs/`',
491
+ `\`command-bindings/\`, \`ui-views/\`, \`collection-templates/\`${extra.kinds.map((k) => `, \`${k}/\``).join('')}`,
542
492
  '(see manifest for channels). data: `data/`. records are `<id>.<suffix>.<ext>`',
543
493
  'files; ids are paths; references are `<collection>/<id>`. run `dreamteamer check` (`npm run',
544
494
  'check`) after bulk edits; run `dreamteamer compile` (`npm run compile`) after changing any',
545
- // ⚠ ONE sentence, APPENDED to the line above rather than given one of its own: this block is
546
- // committed prose in every workspace and its budget is asserted (compile.test.js, "a workspace
547
- // that has added nothing gets a SMALL block"), so a clause that earns its place still may not
548
- // spend a line. It says the one thing a session cannot derive from the schema — that a claim
549
- // an artifact works is a claim about the RUNNING system, and which instrument produces one.
550
- 'source or installing modules. before saying an artifact works, run `dreamteamer prove <artifact>` and quote its result.',
495
+ 'source or installing modules.',
551
496
  ];
497
+ // an installed extension's own paragraph — how an optional tool tells every session it exists
498
+ for (const p of extra.paragraphs) lines.push(p.trim());
552
499
  // ⚠ Only when the workspace HAS namespaces. Telling an agent about a feature this workspace does
553
500
  // not use is the same failure as telling it the wrong source layout — prose that contradicts the
554
501
  // workspace is worse than no prose.
@@ -647,7 +594,7 @@ function pruneEmptyDirs(dir) {
647
594
  for (const name of fs.readdirSync(dir)) {
648
595
  // ⚠ a LIVE linked worktree may sit at `.claude/worktrees/<name>` — a harness can park one
649
596
  // there itself. Its empty directories belong to whoever checked it out; compile must never
650
- // walk it. (`dt land` places worktrees under `.worktrees/` for the same reason.)
597
+ // walk it. (the workflows extension places worktrees under `.worktrees/` for the same reason.)
651
598
  if (name === 'worktrees' && path.basename(dir) === '.claude') continue;
652
599
  const p = path.join(dir, name);
653
600
  if (fs.statSync(p).isDirectory()) pruneEmptyDirs(p);
package/src/init.js CHANGED
@@ -154,13 +154,19 @@ export function init({ flags = {} } = {}) {
154
154
  ensureEditorRecommendation(root);
155
155
  if (!fs.existsSync(path.join(root, '.env.example'))) fs.writeFileSync(path.join(root, '.env.example'), ENV_EXAMPLE);
156
156
 
157
- // one init commit (if we're in a git repo)
157
+ // one init commit (if we're in a git repo) — of the files init WROTE, and nothing else. It used to
158
+ // `git add --all`, which in an existing repo swept every unrelated staged and unstaged change into
159
+ // a commit titled "init workspace": the one write in this engine that was not pathspec-scoped.
160
+ // `commit -- <paths>` also leaves anything the operator had already staged exactly as it was.
161
+ const written = ['package.json', '.gitignore', '.env.example', '.vscode/extensions.json',
162
+ ...(wm ? [path.join('modules', wm, 'package.json')] : []), path.relative(root, starter)]
163
+ .filter((f) => fs.existsSync(path.join(root, f)));
158
164
  try {
159
165
  // stdio ignored on purpose: this whole block is best-effort, and execFileSync forwards the
160
166
  // child's stderr to ours by default — so a plain `dreamteamer init` in a non-git folder
161
167
  // printed git's raw "fatal: not a git repository" above our own handled warning.
162
- execFileSync('git', ['add', '--all'], { cwd: root, stdio: 'ignore' });
163
- execFileSync('git', ['commit', '--quiet', '-m', `dreamteamer: init workspace ${name}`], { cwd: root, stdio: 'ignore' });
168
+ execFileSync('git', ['add', '--', ...written], { cwd: root, stdio: 'ignore' });
169
+ execFileSync('git', ['commit', '--quiet', '-m', `dreamteamer: init workspace ${name}`, '--', ...written], { cwd: root, stdio: 'ignore' });
164
170
  } catch { console.warn('⚠ not a git repo (or nothing to commit) — init files written, no commit'); }
165
171
 
166
172
  console.log(`✔ workspace ${name} initialized — run \`dreamteamer compile\` to materialize the runtime`);