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.
- package/README.md +47 -153
- package/collections/collections.collection.yaml +26 -9
- package/package.json +14 -2
- package/skills/using-dreamteamer/SKILL.md +16 -10
- package/skills/using-dreamteamer/references/before-you-build.md +4 -3
- package/skills/using-dreamteamer/references/collections.md +71 -1
- package/skills/using-dreamteamer/references/commands.md +3 -3
- package/skills/using-dreamteamer/references/data-modeling.md +9 -1
- package/skills/using-dreamteamer/references/extensions.md +60 -0
- package/skills/using-dreamteamer/references/records.md +17 -0
- package/skills/using-dreamteamer/references/sessions.md +4 -5
- package/skills/using-dreamteamer/references/skills.md +3 -5
- package/src/api.d.ts +109 -0
- package/src/api.js +70 -0
- package/src/check.js +57 -2
- package/src/checkout.js +105 -330
- package/src/cli.js +90 -274
- package/src/collections-cli.js +16 -165
- package/src/commit.js +7 -0
- package/src/compile.js +211 -155
- package/src/events.js +7 -2
- package/src/extensions.js +135 -0
- package/src/filter.js +1 -1
- package/src/harnesses.js +82 -135
- package/src/init.js +9 -3
- package/src/placement.js +209 -0
- package/src/records-api.d.ts +103 -0
- package/src/records-api.js +40 -0
- package/src/runtime.js +2 -2
- package/src/schema-ops.js +26 -9
- package/src/store.js +396 -35
- package/collections/containers.collection.yaml +0 -81
- package/collections/images.collection.yaml +0 -46
- package/collections/proofs.collection.yaml +0 -96
- package/skills/using-dreamteamer/references/exporting.md +0 -53
- package/skills/using-dreamteamer/references/proofs.md +0 -435
- package/skills/using-dreamteamer/references/worktrees.md +0 -235
- package/src/container-archive.js +0 -356
- package/src/containers.js +0 -635
- package/src/export-notebooklm.js +0 -502
- package/src/land.js +0 -743
- package/src/prove.js +0 -1922
- 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
|
|
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'
|
|
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
|
|
23
|
-
// whether its hunks lie between them
|
|
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
|
-
|
|
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 (!
|
|
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') ?
|
|
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') ?
|
|
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') ?
|
|
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}${
|
|
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
|
-
// ----
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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: []
|
|
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' :
|
|
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(/\.
|
|
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
|
|
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
|
|
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(' · ')}.` : ''}
|
|
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
|
-
|
|
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
|
-
|
|
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. (
|
|
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', '--
|
|
163
|
-
execFileSync('git', ['commit', '--quiet', '-m', `dreamteamer: init workspace ${name}
|
|
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`);
|