dreamteamer 0.9.1 → 0.10.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.
@@ -11,6 +11,17 @@ schema:
11
11
  description:
12
12
  type: string
13
13
  description: What kind of thing this collection holds, in one line.
14
+ use_when:
15
+ type: string
16
+ description: >-
17
+ WHEN to reach for this collection, in one clause — rendered into the orientation block every
18
+ agent session loads. OPTIONAL and usually absent: for most collections the trigger IS the
19
+ description (`contacts` = a person; you use it when you have a person), and a restatement
20
+ there costs every session tokens while diluting the few that carry real information. The
21
+ test: author this only when an agent that FULLY UNDERSTANDS the description would still not
22
+ know to reach for the collection. It spans both acts — read ("you are about to diagnose a
23
+ defect — search here first") and write ("a day needs planning"). NOT a place for procedure:
24
+ a `how` belongs in the module's skill, this field holds a `when`.
14
25
  title:
15
26
  type: string
16
27
  description: What to call this collection in the nav and page headers. DERIVED from `name` by title-casing when absent — author it only when that is wrong (`ui-views` → `UI Views`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamteamer",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "A workspace compiler for coding agents \u2014 schema-validated records as plain files over git, compiled into every harness",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Gilad Khen <giladkhen@gmail.com>",
package/src/compile.js CHANGED
@@ -696,6 +696,17 @@ export function compile({ root, pkg }) {
696
696
  }
697
697
  const rt = path.join('collections', `${name}.collection.yaml`);
698
698
  entries.set(rt, { sources: [...group.map((g) => g.src), ...templateSources], bytes: Buffer.from(dump(merged)) });
699
+ // A descriptor with no `description:` renders in the orientation block as a bare NAME — an
700
+ // agent learns the noun exists and nothing about when it is the right one. Derived pressure
701
+ // rather than a heroic backfill pass, and the same shape as the per-missing-env-key warning:
702
+ // non-blocking, named per offender, so the gap converges instead of being rediscovered.
703
+ //
704
+ // ⚠ Deliberately NO equivalent warning for `use_when`. That field is optional and correct to
705
+ // omit on most collections — warning on it would invert its authoring test and manufacture a
706
+ // restatement of the description on every collection that does not need one.
707
+ if (merged.storage.base !== 'runtime' && !String(merged.description ?? '').trim()) {
708
+ console.warn(`⚠ collection ${name} has no description — it renders as a bare name in the orientation block every session loads`);
709
+ }
699
710
  counts.collections++;
700
711
  if (extenders.length) mergedCount++;
701
712
  }
package/src/harnesses.js CHANGED
@@ -56,7 +56,7 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
56
56
  }
57
57
  summary.push(`claude-code → .claude (${n} files)`);
58
58
  }
59
- writeBlock(root, 'CLAUDE.md', on('claude-code') ? orientationBlock('claude-code', skillsIndex, sourceLayout, namespaces, version) : null);
59
+ writeBlock(root, 'CLAUDE.md', on('claude-code') ? orientationBlock('claude-code', skillsIndex, sourceLayout, namespaces, version, entries) : null);
60
60
 
61
61
  // ---- shared cross-agent skills mirror (.agents/skills) — codex/pi discover it,
62
62
  // cursor/gemini blocks point at it. written once no matter how many harnesses use it.
@@ -73,17 +73,17 @@ export function runHarnessAdapters({ root, entries, harnesses, prevManifest, sou
73
73
  }
74
74
 
75
75
  // ---- codex + pi: both read root AGENTS.md; one block serves both ----------------
76
- writeBlock(root, 'AGENTS.md', on('codex') || on('pi') ? orientationBlock('agents-md', skillsIndex, sourceLayout, namespaces, version) : null);
76
+ writeBlock(root, 'AGENTS.md', on('codex') || on('pi') ? orientationBlock('agents-md', skillsIndex, sourceLayout, namespaces, version, entries) : null);
77
77
  if (on('codex')) summary.push('codex → AGENTS.md block');
78
78
  if (on('pi')) summary.push('pi → AGENTS.md block + .agents/skills');
79
79
 
80
80
  // ---- gemini-cli: GEMINI.md is its context file -----------------------------------
81
- writeBlock(root, 'GEMINI.md', on('gemini-cli') ? orientationBlock('gemini', skillsIndex, sourceLayout, namespaces, version) : null);
81
+ writeBlock(root, 'GEMINI.md', on('gemini-cli') ? orientationBlock('gemini', skillsIndex, sourceLayout, namespaces, version, entries) : null);
82
82
  if (on('gemini-cli')) summary.push('gemini-cli → GEMINI.md block');
83
83
 
84
84
  // ---- cursor: native .mdc rule (alwaysApply) ---------------------------------------
85
85
  if (on('cursor')) {
86
- const mdc = `---\ndescription: dreamteamer workspace orientation (generated)\nalwaysApply: true\n---\n\n${orientationBlock('cursor', skillsIndex, sourceLayout, namespaces, version)}\n\n${STAMP}\n`;
86
+ const mdc = `---\ndescription: dreamteamer workspace orientation (generated)\nalwaysApply: true\n---\n\n${orientationBlock('cursor', skillsIndex, sourceLayout, namespaces, version, entries)}\n\n${STAMP}\n`;
87
87
  write('.cursor/rules/dreamteamer.mdc', Buffer.from(mdc));
88
88
  summary.push('cursor → .cursor/rules/dreamteamer.mdc');
89
89
  }
@@ -123,6 +123,161 @@ function buildSkillsIndex(entries) {
123
123
  return index.sort((a, b) => a.id.localeCompare(b.id));
124
124
  }
125
125
 
126
+ /** One flattened line of prose from a possibly-folded YAML scalar. */
127
+ const flat = (s) => String(s ?? '').replace(/\s+/g, ' ').trim();
128
+
129
+ /** collection name -> description / use_when / whether it is a schema-ops collection, read from the
130
+ * compiled descriptor BYTES compile already holds. Same pattern as buildSkillsIndex: no filesystem
131
+ * read, and deliberately nothing that touches `data/` — see collectionsSection for why. */
132
+ function buildCollectionsIndex(entries) {
133
+ const index = [];
134
+ for (const [rt, e] of entries) {
135
+ const m = /^collections\/(.+)\.collection\.yaml$/.exec(rt);
136
+ if (!m) continue;
137
+ let d = {};
138
+ try { d = load(e.bytes.toString('utf8')) ?? {}; } catch { /* unparseable descriptor */ }
139
+ index.push({
140
+ name: d.name ?? m[1],
141
+ // DERIVED, never a hardcoded name list: `runtime` is exactly the schema-ops set
142
+ // (collections, commands, skills, agents, ui-views, command-bindings,
143
+ // collection-templates, modules) and stays right in a workspace shipping others.
144
+ system: d.storage?.base === 'runtime',
145
+ description: flat(d.description),
146
+ useWhen: flat(d.use_when),
147
+ });
148
+ }
149
+ return index.sort((a, b) => a.name.localeCompare(b.name));
150
+ }
151
+
152
+ /** Commands, for the harnesses that do NOT discover them natively. claude-code writes every command
153
+ * into `.claude/commands` and its harness injects name+description at session start, so an index in
154
+ * CLAUDE.md would be a second copy of what is already there. codex/pi/gemini-cli/cursor got one
155
+ * pointer line and nothing else — which is why this is conditional, like the skills index. */
156
+ function buildCommandsIndex(entries) {
157
+ const index = [];
158
+ for (const [rt, e] of entries) {
159
+ const m = /^commands\/(.+)\.command\.md$/.exec(rt);
160
+ if (!m) continue;
161
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(e.bytes.toString('utf8'));
162
+ let desc = '';
163
+ try { desc = (fm ? load(fm[1]) : {})?.description ?? ''; } catch { /* unparseable frontmatter */ }
164
+ index.push({ id: m[1], desc: flat(desc) });
165
+ }
166
+ return index.sort((a, b) => a.id.localeCompare(b.id));
167
+ }
168
+
169
+ /** THE LEXICON — the half of the DSL that was never at t=0. The grammar (record shape, reference
170
+ * shape, namespaces) has been in this block since 0.6; an agent could form a well-shaped reference
171
+ * and not name one thing it could be about. The dogfood vault's hand-written list in CLAUDE.md is
172
+ * the argument for deriving it: that list named three collections for 48 hours after they were
173
+ * deleted, because a human had to remember to edit it.
174
+ *
175
+ * ⚠ NO RECORD COUNTS, and that is not an omission. This block lands in CLAUDE.md / AGENTS.md /
176
+ * GEMINI.md, which are COMMITTED files. A count changes on every write, so every compile after
177
+ * ordinary data work would re-dirty three tracked files — in trees where more than one agent has
178
+ * uncommitted work — and the number would be stale the moment it was printed. Ordering is
179
+ * namespace-grouped then alphabetical precisely so the block diffs ONLY when the schema changed. */
180
+ function collectionsSection(index, namespaces) {
181
+ if (!index.length) return [];
182
+ const nsOf = (name) => {
183
+ const i = name.indexOf('/');
184
+ const p = i < 0 ? '' : name.slice(0, i);
185
+ return namespaces.includes(p) ? p : '';
186
+ };
187
+ const lines = [
188
+ '',
189
+ 'COLLECTIONS — the nouns of this workspace. The descriptor at',
190
+ '`.dreamteamer/collections/<name>.collection.yaml` is the authority on fields, id shape and',
191
+ 'defaults — read it before writing a kind you have not written this session. Create records',
192
+ 'with `dt <collection> add`: it generates the id and rejects invalid writes before disk.',
193
+ ];
194
+ const data = index.filter((c) => !c.system);
195
+ for (const group of ['', ...namespaces]) {
196
+ for (const c of data.filter((c) => nsOf(c.name) === group)) {
197
+ lines.push(`- ${c.name}${c.description ? ` — ${c.description}` : ''}`);
198
+ if (c.useWhen) lines.push(` use when: ${c.useWhen}`);
199
+ }
200
+ }
201
+ const system = index.filter((c) => c.system).map((c) => c.name);
202
+ if (system.length) lines.push(`- schema-ops only (write with the meta verbs, never by hand): ${system.join(' · ')}`);
203
+ return lines;
204
+ }
205
+
206
+ /** Collection-templates are field sets modules stamp onto their collections — the provenance quad
207
+ * reaches ~40 collections in the dogfood vault and was stated nowhere an agent reads.
208
+ *
209
+ * ⚠ RENDERED, NEVER HARDCODED. An earlier draft wrote the four provenance field names straight
210
+ * into this file. That is recipe knowledge in core: `provenance` is shipped by a MODULE, and
211
+ * deliberately triplicated (`provenance`, `crm-provenance`, `rnd-provenance`) so each module stays
212
+ * copyable on its own. A workspace without them would have been handed a block that lies. Core
213
+ * renders the line; the module owns the sentence. */
214
+ function templatesSection(entries) {
215
+ const rows = [];
216
+ for (const [rt, e] of entries) {
217
+ const m = /^collection-templates\/(.+)\.collection-template\.yaml$/.exec(rt);
218
+ if (!m) continue;
219
+ let d = {};
220
+ try { d = load(e.bytes.toString('utf8')) ?? {}; } catch { /* unparseable template */ }
221
+ rows.push({ name: d.name ?? m[1], description: flat(d.description) });
222
+ }
223
+ if (!rows.length) return [];
224
+ rows.sort((a, b) => a.name.localeCompare(b.name));
225
+ return ['', 'CROSS-CUTTING TEMPLATES — field sets modules stamp onto their collections:',
226
+ ...rows.map((r) => `- ${r.name}${r.description ? ` — ${r.description}` : ''}`)];
227
+ }
228
+
229
+ /** `can-enter` / `can-exit` rendered LITERALLY. Translating a filter into English would be a
230
+ * generator that has to grow with every operator the filter grammar gains, for prose the binding's
231
+ * own `description:` already carries. Real shapes are a nested map terminating in one `_op`:
232
+ * {status: {_eq: draft}} -> status=draft
233
+ * {status: {_in: [enriched, x]}} -> status in enriched|x
234
+ * {file: {_nempty: true}} -> file set
235
+ * {meeting: {_empty: true}} -> meeting empty */
236
+ const GATE_OPS = {
237
+ _eq: (p, v) => `${p}=${v}`,
238
+ _in: (p, v) => `${p} in ${[].concat(v).join('|')}`,
239
+ _nempty: (p) => `${p} set`,
240
+ _empty: (p) => `${p} empty`,
241
+ };
242
+
243
+ function renderGate(label, filter) {
244
+ const parts = [];
245
+ const walkFilter = (obj, at) => {
246
+ for (const [k, v] of Object.entries(obj ?? {})) {
247
+ if (k.startsWith('_')) parts.push((GATE_OPS[k] ?? ((p, x) => `${p} ${k.slice(1)} ${x}`))(at, v));
248
+ else if (v && typeof v === 'object') walkFilter(v, at ? `${at}.${k}` : k);
249
+ else parts.push(`${at ? `${at}.` : ''}${k}=${v}`);
250
+ }
251
+ };
252
+ if (filter && typeof filter === 'object') walkFilter(filter, '');
253
+ return parts.length ? `${label}: ${parts.join(', ')}` : '';
254
+ }
255
+
256
+ /** The METHODS on a type. A binding says which collection a command applies to and the record state
257
+ * that makes it available — invisible on EVERY harness, claude-code included, whose native command
258
+ * discovery gives the command's description and nothing about what it acts on. The cost of the
259
+ * absence is on disk: a contact record in the dogfood vault carries the hand-written sentence
260
+ * "`status: draft` keeps this in the /scrape-contact queue" — an agent re-deriving a can-enter
261
+ * filter into prose because nothing surfaced it. */
262
+ function bindingsSection(entries) {
263
+ const byCollection = new Map();
264
+ for (const [rt, e] of entries) {
265
+ if (!/^command-bindings\/.+\.command-binding\.yaml$/.test(rt)) continue;
266
+ let d = {};
267
+ try { d = load(e.bytes.toString('utf8')) ?? {}; } catch { continue; }
268
+ const coll = String(d.collection ?? '').replace(/^collections\//, '');
269
+ const cmd = String(d.command ?? '').replace(/^commands\//, '');
270
+ if (!coll || !cmd) continue;
271
+ const gate = [renderGate('enter', d['can-enter']), renderGate('exit', d['can-exit'])].filter(Boolean).join(' · ');
272
+ if (!byCollection.has(coll)) byCollection.set(coll, []);
273
+ byCollection.get(coll).push(`/${cmd}${gate ? ` (${gate})` : ''}`);
274
+ }
275
+ if (!byCollection.size) return [];
276
+ const lines = ['', 'VERBS BOUND TO COLLECTIONS (`dt commands for <collection>[/<id>]` answers per record):'];
277
+ for (const coll of [...byCollection.keys()].sort()) lines.push(`- ${coll} — ${byCollection.get(coll).sort().join(' · ')}`);
278
+ return lines;
279
+ }
280
+
126
281
  /** `sourceLayout` describes what THIS workspace actually looks like — 'flat' (`<module>/skills/`),
127
282
  * 'nested' (the pre-2026-08-05 `<module>/system/skills/`), or 'mixed'. It is passed in rather than
128
283
  * assumed because generated prose that contradicts the workspace is worse than no prose: this block
@@ -134,7 +289,7 @@ function buildSkillsIndex(entries) {
134
289
  * agent that splits `health/doctors/dana-levi` at the first slash reads a collection that does not
135
290
  * exist. Naming the declared list is what makes the grammar decidable from this block alone, without
136
291
  * the agent having to go read the manifest. A workspace with no namespaces gets no extra sentence. */
137
- function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces = [], version = 'unknown') {
292
+ function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces = [], version = 'unknown', entries = new Map()) {
138
293
  const sourcesLine = {
139
294
  flat: '`modules/<module>/<kind>/` — `collections/`, `skills/`, `agents/`, `commands/`,',
140
295
  nested: '`modules/<module>/system/<kind>/` — `collections/`, `skills/`, `agents/`, `commands/`,',
@@ -145,7 +300,8 @@ function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces
145
300
  // exactly one minor release and then quietly wrong in every workspace it had been written into.
146
301
  // Passed rather than imported because compile.js already computes it and imports THIS module —
147
302
  // reaching back for `engineVersion` would close a cycle for the sake of one string.
148
- `this workspace is operated by dreamteamer v${version}. **read the \`using-dreamteamer\` skill before`,
303
+ `this workspace is a typed record DSL operated by dreamteamer v${version} collections are the`,
304
+ 'nouns. **read the `using-dreamteamer` skill before',
149
305
  'working with data.** schemas (read): `.dreamteamer/collections/` (provenance:',
150
306
  '`.dreamteamer/manifest.yaml`). sources (write): ' + sourcesLine,
151
307
  '`command-bindings/`, `ui-views/`, `collection-templates/`',
@@ -166,6 +322,9 @@ function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces
166
322
  'DECLARED prefix, not at the first slash. collections with no prefix are unaffected.',
167
323
  );
168
324
  }
325
+ lines.push(...collectionsSection(buildCollectionsIndex(entries), namespaces));
326
+ lines.push(...templatesSection(entries));
327
+ lines.push(...bindingsSection(entries));
169
328
  // claude-code discovers skills natively (Skill tool) — an index in CLAUDE.md is pure
170
329
  // context bloat there. every other harness gets the trigger index + discovery pointers.
171
330
  if (flavor !== 'claude-code') {
@@ -177,7 +336,8 @@ function orientationBlock(flavor, skillsIndex, sourceLayout = 'flat', namespaces
177
336
  ...skillsIndex.map((s) => `- \`${s.id}\` — ${s.desc}`),
178
337
  '',
179
338
  'agent personas live at `.dreamteamer/agents/*.agent.md`; commands at',
180
- '`.dreamteamer/commands/` (invoked as `/<name>` by the harness).',
339
+ '`.dreamteamer/commands/`, invoked as `/<name>` by the harness:',
340
+ ...buildCommandsIndex(entries).map((c) => `- \`/${c.id}\` — ${c.desc}`),
181
341
  );
182
342
  }
183
343
  return lines.join('\n');