dreamteamer 0.27.0 → 0.29.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 +3 -0
- package/collections/collections.collection.yaml +23 -0
- package/collections/containers.collection.yaml +81 -0
- package/collections/images.collection.yaml +46 -0
- package/package.json +4 -3
- package/skills/using-dreamteamer/SKILL.md +1 -0
- package/skills/using-dreamteamer/references/getting-started.md +21 -0
- package/src/cli.js +96 -12
- package/src/collections-cli.js +10 -0
- package/src/commit.js +3 -2
- package/src/compile.js +115 -5
- package/src/containers.js +492 -0
- package/src/harnesses.js +57 -9
- package/src/init.js +4 -1
- package/src/ref.js +28 -6
- package/src/schema-ops.js +1 -0
- package/src/server.js +28 -0
- package/src/workspace.js +50 -0
package/src/compile.js
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
baseNameOf, singular, namespaceOf } from './namespace.js';
|
|
17
17
|
// circular on paper in earlier versions — safe: both sides only
|
|
18
18
|
// call at run time, same pattern as store.js ↔ compile.js.
|
|
19
|
-
import { runHarnessAdapters } from './harnesses.js';
|
|
19
|
+
import { runHarnessAdapters, BEGIN, END, INSTRUCTIONS_BEGIN, INSTRUCTIONS_END } from './harnesses.js';
|
|
20
|
+
import { ensureEditorRecommendation, ensureEnvExample } from './workspace.js';
|
|
20
21
|
import { satisfies } from './semver.js';
|
|
21
22
|
import { parseEnvValues } from './env-vars.js';
|
|
22
23
|
import { DERIVED_KINDS, readManifest, runtimeDir, engineId, engineVersion } from './runtime.js';
|
|
@@ -676,7 +677,9 @@ export function compile({ root, pkg }) {
|
|
|
676
677
|
// what every message in this engine already calls it. Defined HERE, above the namespace pass,
|
|
677
678
|
// because a namespace error has to name the module by the id the fix is typed with.
|
|
678
679
|
const moduleId = (n) => slug(String(n).replace(/^@[^/]+\//, ''));
|
|
680
|
+
const channelOf = new Map(sources.map((s) => [s.name, s.channel]));
|
|
679
681
|
const declaredEnv = new Map(); // env key -> [module names]
|
|
682
|
+
const envMeta = new Map(); // env key -> { description, example } — the first module to say wins
|
|
680
683
|
const moduleIgnores = new Map(); // module name -> non-source folders it declares (strayKindDirs)
|
|
681
684
|
const moduleDeps = new Map(); // module name -> [module names] — HARD, must be acyclic
|
|
682
685
|
const modulePeers = new Map(); // module name -> [collection names] — SOFT, cannot cycle
|
|
@@ -717,9 +720,17 @@ export function compile({ root, pkg }) {
|
|
|
717
720
|
if (ok === false) console.warn(`⚠ module ${source.name} declares engine "${range}" — running engine is ${engineVer} (out of range; compile continues)`);
|
|
718
721
|
else if (ok === null) console.warn(`⚠ module ${source.name}: engine range "${range}" not understood by the built-in checker (see src/semver.js) — not verified`);
|
|
719
722
|
}
|
|
720
|
-
|
|
723
|
+
// `dreamteamer.env`: a bare key name, or `{ name, description, example }` so the warning and
|
|
724
|
+
// `.env.example` can say what the key IS and what a value looks like — a bare `WORK_CALENDARS`
|
|
725
|
+
// told a first-run operator nothing about ids, addresses or display names (2026-09-24).
|
|
726
|
+
const envDecl = mpkg.dreamteamer?.env ?? [];
|
|
727
|
+
if (!Array.isArray(envDecl)) fail(`module "${source.name}": dreamteamer.env must be a list of key names or { name, description, example } objects (got ${JSON.stringify(envDecl)})`);
|
|
728
|
+
for (const entry of envDecl) {
|
|
729
|
+
const k = typeof entry === 'string' ? entry : entry?.name;
|
|
730
|
+
if (typeof k !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) fail(`module "${source.name}": dreamteamer.env entry ${JSON.stringify(entry)} — a key is an identifier (A-Z, 0-9, _) as a string or as { name, description, example }`);
|
|
721
731
|
if (!declaredEnv.has(k)) declaredEnv.set(k, []);
|
|
722
732
|
declaredEnv.get(k).push(source.name);
|
|
733
|
+
if (typeof entry === 'object' && !envMeta.has(k)) envMeta.set(k, { description: entry.description ? String(entry.description) : undefined, example: entry.example !== undefined ? String(entry.example) : undefined });
|
|
723
734
|
}
|
|
724
735
|
// Gathered here because mpkg is already parsed; refused below, next to the workspace's own
|
|
725
736
|
// declaration. The classic layout pushes the ROOT itself as an inline source, whose
|
|
@@ -758,7 +769,8 @@ export function compile({ root, pkg }) {
|
|
|
758
769
|
const present = new Set([...parsedEnv].filter(([, v]) => v.trim() !== '').map(([k]) => k));
|
|
759
770
|
for (const [k, mods] of declaredEnv) {
|
|
760
771
|
if (present.has(k)) continue;
|
|
761
|
-
|
|
772
|
+
const about = envMeta.get(k)?.description ? ` (${envMeta.get(k).description})` : '';
|
|
773
|
+
for (const mod of mods) console.warn(`⚠ module ${mod} declares env key ${k}${about} — missing from .env (see .env.example)`);
|
|
762
774
|
}
|
|
763
775
|
for (const k of declaredVars) {
|
|
764
776
|
if (present.has(k)) continue;
|
|
@@ -767,6 +779,17 @@ export function compile({ root, pkg }) {
|
|
|
767
779
|
}
|
|
768
780
|
}
|
|
769
781
|
|
|
782
|
+
// Two root files kept current on every compile, both cheap and both about the FIRST run of a
|
|
783
|
+
// stranger: `.env.example` lists every declared key with its description, so the warning above
|
|
784
|
+
// points at a file that actually names them; `.vscode/extensions.json` recommends the editor
|
|
785
|
+
// extension, so the first window offers it. Both are append/merge-only — nothing authored moves.
|
|
786
|
+
{
|
|
787
|
+
const added = ensureEnvExample(root, [...declaredEnv].map(([key, mods]) => ({ key, modules: mods, ...(envMeta.get(key) ?? {}) })),
|
|
788
|
+
'# secrets for skills and modules go here (copy to .env; .env is never committed).\n# modules declare the env keys they require in their package.json dreamteamer.env list.\n');
|
|
789
|
+
if (added.length) console.log(`✔ .env.example now names ${added.join(', ')}`);
|
|
790
|
+
ensureEditorRecommendation(root);
|
|
791
|
+
}
|
|
792
|
+
|
|
770
793
|
// ---- local-assets and postinstall: what `dt install` will do to a checkout -------
|
|
771
794
|
// `local-assets` are the gitignored heavy folders a checkout SHARES by symlink instead of
|
|
772
795
|
// duplicating — a browser profile dir, a model cache. Declared, never discovered. Every
|
|
@@ -1093,6 +1116,7 @@ export function compile({ root, pkg }) {
|
|
|
1093
1116
|
let mergedCount = 0;
|
|
1094
1117
|
let templatedCount = 0;
|
|
1095
1118
|
const storageEntries = []; // {name, path, base} per collection — checked for overlap after the loop
|
|
1119
|
+
const wordEntries = []; // {name, word: singular} per collection — checked for collisions after the loop
|
|
1096
1120
|
// Merged descriptors are held, NOT dumped, until every one of them exists: a relation spans two
|
|
1097
1121
|
// collections, and the second is not merged yet when the first is reached. So this loop resolves
|
|
1098
1122
|
// and validates each descriptor on its own, `materializeRelations` runs over the whole set, and
|
|
@@ -1285,8 +1309,14 @@ export function compile({ root, pkg }) {
|
|
|
1285
1309
|
if (raw === '*') {
|
|
1286
1310
|
// The workspace module is the orchestrating parent and may reference anything —
|
|
1287
1311
|
// including modules that do not exist yet, which is what `tasks.item` means.
|
|
1288
|
-
// Anywhere else a wildcard is a cross-module surface no declaration can cover
|
|
1289
|
-
|
|
1312
|
+
// Anywhere else a wildcard is a cross-module surface no declaration can cover — and
|
|
1313
|
+
// it is the MODULE AUTHOR's to cover, so the warning is raised only where the author
|
|
1314
|
+
// is: a module in this tree (inline). A module installed from npm or a clone is
|
|
1315
|
+
// somebody else's source; warning its consumers about it on every compile told a
|
|
1316
|
+
// first-run operator four things they could not fix (2026-09-24). The module's own
|
|
1317
|
+
// CI, compiling it alone, still sees them.
|
|
1318
|
+
const authoredHere = groupModules.some((m) => (channelOf.get(m) ?? 'inline') === 'inline');
|
|
1319
|
+
if (!groupModules.includes(wsModuleName) && authoredHere) {
|
|
1290
1320
|
console.warn(`⚠ collection ${name}: field "${at}" uses x-reference: '*' outside the workspace module — an unverifiable cross-module surface; name the collections it may target`);
|
|
1291
1321
|
}
|
|
1292
1322
|
continue;
|
|
@@ -1380,6 +1410,16 @@ export function compile({ root, pkg }) {
|
|
|
1380
1410
|
// for `meta.title_field`, promoted to an authorable field. Reference fields pointing here
|
|
1381
1411
|
// inherit it (presentation.js), which is what replaces 51 hand-written `x-display` lines.
|
|
1382
1412
|
merged.title_template ??= `{{ ${['title', 'name', 'subject'].find((f) => f in labelProps) ?? 'id'} }}`;
|
|
1413
|
+
// The word the CLI accepts beside the name (`dt add task …`). DERIVED by the same inflection
|
|
1414
|
+
// the storage suffix already uses, with the namespace kept (`rnd/projects` → `rnd/project`),
|
|
1415
|
+
// so the two never disagree; AUTHORED where inflection is wrong (`people` → `person`).
|
|
1416
|
+
// Collisions are refused after the loop, once every descriptor has one.
|
|
1417
|
+
if (merged.singular !== undefined && (typeof merged.singular !== 'string' || !merged.singular.trim())) fail(`collection "${name}": \`singular\` must be a non-empty string`);
|
|
1418
|
+
if (merged.singular === undefined) {
|
|
1419
|
+
const ns = namespaceOf(name, namespaces);
|
|
1420
|
+
merged.singular = ns ? `${ns}/${singular(baseNameOf(name, namespaces))}` : singular(name);
|
|
1421
|
+
}
|
|
1422
|
+
wordEntries.push({ name, word: merged.singular });
|
|
1383
1423
|
for (const [fieldName, prop] of Object.entries(labelProps)) {
|
|
1384
1424
|
if (!prop || typeof prop !== 'object' || Array.isArray(prop)) continue;
|
|
1385
1425
|
prop.title ??= titleCase(fieldName);
|
|
@@ -1445,6 +1485,19 @@ export function compile({ root, pkg }) {
|
|
|
1445
1485
|
// `owns-data` module prefix and any authored override all already applied). See
|
|
1446
1486
|
// namespace.storageOverlaps for what this silently did before it was checked.
|
|
1447
1487
|
for (const p of storageOverlaps(storageEntries)) fail(p);
|
|
1488
|
+
// Two collections that answer to one word would make `dt add <word>` a coin toss, so the set of
|
|
1489
|
+
// words — every name and every singular — must be injective. Refused with both names, because
|
|
1490
|
+
// the fix is an authored `singular:` on one of them and the author needs to know which two.
|
|
1491
|
+
{
|
|
1492
|
+
const owners = new Map(); // word -> name
|
|
1493
|
+
for (const { name } of wordEntries) owners.set(name, name);
|
|
1494
|
+
for (const { name, word } of wordEntries) {
|
|
1495
|
+
if (word === name) continue;
|
|
1496
|
+
const other = owners.get(word);
|
|
1497
|
+
if (other && other !== name) fail(`collections "${name}" and "${other}" both answer to the word "${word}" (a name or a singular) — author \`singular:\` on one of them so \`dt add ${word}\` names exactly one collection`);
|
|
1498
|
+
owners.set(word, name);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1448
1501
|
|
|
1449
1502
|
// ---- modules, projected ---------------------------------------------------------
|
|
1450
1503
|
// One record per discovered module, written from what discovery and the package pass already
|
|
@@ -1526,6 +1579,19 @@ export function compile({ root, pkg }) {
|
|
|
1526
1579
|
counts.modules = (counts.modules ?? 0) + 1;
|
|
1527
1580
|
}
|
|
1528
1581
|
|
|
1582
|
+
// ---- the workspace's own hand-written instructions -------------------------------
|
|
1583
|
+
// ONE source, rendered verbatim into every harness's instruction file. It is registered as a
|
|
1584
|
+
// manifest entry for exactly one reason: `staleness` walks manifest sources, so a file that is
|
|
1585
|
+
// not one can be edited forever without `dt status` ever saying the harness files lag it — and a
|
|
1586
|
+
// silent lag on the file carrying the operator's rules is the worst possible thing to be silent
|
|
1587
|
+
// about. The runtime copy is never read by anything; the manifest ENTRY is the whole point.
|
|
1588
|
+
const instructionsPath = path.join(root, INSTRUCTIONS_SOURCE);
|
|
1589
|
+
if (fs.existsSync(instructionsPath)) {
|
|
1590
|
+
const bytes = fs.readFileSync(instructionsPath);
|
|
1591
|
+
refuseManagedMarkers(bytes.toString('utf8'), rel(instructionsPath));
|
|
1592
|
+
entries.set('instructions.md', { sources: [{ path: rel(instructionsPath), hash: sha256(bytes) }], bytes });
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1529
1595
|
// ---- unresolved references are compile errors (an agent's declared skills)
|
|
1530
1596
|
const skillIds = new Set([...entries.keys()].filter((k) => k.startsWith('skills/')).map((k) => k.split('/')[1]));
|
|
1531
1597
|
for (const [rt, e] of entries) {
|
|
@@ -1877,6 +1943,14 @@ export function staleness(root) {
|
|
|
1877
1943
|
}
|
|
1878
1944
|
}
|
|
1879
1945
|
}
|
|
1946
|
+
// ⚠ `dreamteamer.md` is a compile source that is NOT under a KIND directory, so the walk above
|
|
1947
|
+
// cannot reach it — and its CREATION is the one moment that matters most: day one in an adopting
|
|
1948
|
+
// workspace, when no harness file carries an instructions block yet. Every later EDIT was already
|
|
1949
|
+
// caught by the manifest-source walk at the top of this function; only the first write was silent,
|
|
1950
|
+
// and it reported `.dreamteamer is fresh` while the rules reached no agent at all.
|
|
1951
|
+
if (fs.existsSync(path.join(root, INSTRUCTIONS_SOURCE)) && !known.has(INSTRUCTIONS_SOURCE)) {
|
|
1952
|
+
stale.push(`${INSTRUCTIONS_SOURCE} (new, uncompiled)`);
|
|
1953
|
+
}
|
|
1880
1954
|
return { compiled: true, stale, manifest };
|
|
1881
1955
|
}
|
|
1882
1956
|
|
|
@@ -1965,6 +2039,42 @@ function descriptorAjv() {
|
|
|
1965
2039
|
return _descriptorAjv;
|
|
1966
2040
|
}
|
|
1967
2041
|
|
|
2042
|
+
// ⚠ A MANAGED MARKER INSIDE `dreamteamer.md` IS A REFUSAL, not something to escape around.
|
|
2043
|
+
// The file is rendered VERBATIM into a managed block, and `writeBlock` finds that block by the FIRST
|
|
2044
|
+
// occurrence of its begin marker anywhere in the file — so a marker quoted inside the rendered text
|
|
2045
|
+
// is found before the real delimiter. Both directions were measured on a fixture:
|
|
2046
|
+
//
|
|
2047
|
+
// - quoting the ORIENTATION pair: the orientation pass rewrites the quoted region, the instructions
|
|
2048
|
+
// pass that runs immediately after restores it from source, and the real orientation block is
|
|
2049
|
+
// never touched again. It silently keeps describing the schema of the day it was written, while
|
|
2050
|
+
// `compile` exits 0 and `status` reports the runtime fresh.
|
|
2051
|
+
// - quoting the INSTRUCTIONS end marker: the block is closed at the quote and a second end line is
|
|
2052
|
+
// appended, so all three committed root files grow by ~40 bytes and one duplicated line per
|
|
2053
|
+
// compile, without ever reaching a fixed point.
|
|
2054
|
+
//
|
|
2055
|
+
// Escaping the markers on the way out is the alternative, and it is not one: the whole promise of
|
|
2056
|
+
// this file is that what was written is what every agent reads, and an escaped marker is not that.
|
|
2057
|
+
// A rule ABOUT the block describes it instead of quoting it.
|
|
2058
|
+
/** The one hand-written root source. Named once: `compile` reads it and `staleness` looks for it. */
|
|
2059
|
+
export const INSTRUCTIONS_SOURCE = 'dreamteamer.md';
|
|
2060
|
+
|
|
2061
|
+
const MANAGED_MARKERS = [
|
|
2062
|
+
['the orientation block', BEGIN],
|
|
2063
|
+
['the orientation block', END],
|
|
2064
|
+
['the instructions block', INSTRUCTIONS_BEGIN],
|
|
2065
|
+
['the instructions block', INSTRUCTIONS_END],
|
|
2066
|
+
];
|
|
2067
|
+
|
|
2068
|
+
function refuseManagedMarkers(text, srcPath) {
|
|
2069
|
+
const lines = text.split('\n');
|
|
2070
|
+
for (const [i, line] of lines.entries()) {
|
|
2071
|
+
for (const [which, marker] of MANAGED_MARKERS) {
|
|
2072
|
+
if (!line.includes(marker)) continue;
|
|
2073
|
+
fail(`${srcPath}:${i + 1}: contains the managed marker ${marker}, which delimits ${which} in the harness files. This source is rendered verbatim into that block, so the quoted copy is found before the real delimiter and the block is rewritten around the wrong place. Describe the block instead of quoting its marker.`);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
1968
2078
|
function fail(msg) {
|
|
1969
2079
|
throw new CompileError(`compile error: ${msg}`);
|
|
1970
2080
|
}
|